首先,回文相关题目在leetcode可不止这几道,随便搜索就很多:
肯定也不止这些,
回文串(正着读和反着读都一样的字符串,比如aba和abba就是回文串,abac就不是回文串)问题。注意回文串的长度可能是奇数也可能是偶数,解决思路就是双指针。
就拿这篇开个头吧。
开唠~
1.在字符串中找一个最长的回文子串
class Solution {
//时间复杂度为O(N^2),最坏情况下两个指针要把整个字符串走两遍哦,空间复杂度为O(1)
public String longestPalindrome(String s) {
//官话判空
if(s == null || s.length() == 0){
return "";
}
int left = 0;
int right = 0;
for(int i = 0; i < s.length(); i++){
//为什么要扩散两遍,就是因为字符串的长度有奇数也有偶数,所以只从s[i]向两边扩散的话s[i]没有,那怎么办呀对不对
//以s[i]和s[i]为中心向外扩散得到的最长回文子串
int length_odd = expandFromCenter(s, i, i);
//以s[i]和s[i + 1]为中心向外扩散得到的最长回文子串
int length_even = expandFromCenter(s, i, i + 1);
//在result、length1、length2三个中挑出最长的回文子串
//也可以这样写:int result = Math.max(length_odd, length_even);
int result = (length_odd > length_even) ? length_odd : length_even;
//计算对应最大回文子串的起点和终点
if(result > (right - left)){
left = i - (result - 1) / 2;
right = i + result / 2;
}
}
//这里的right+1是因为 java自带的左闭右开的原因
return s.substring(left, right + 1);
}
//函数传入两个指针left和right,
public int expandFromCenter(String s, int left, int right){
//如果当两个指针指向的两边的字母相同,也就是s.charAt(left) == s.charAt(right),我们就可以继续扩展。如果两边的字母不同,我们就可以停止扩展,因为在这之后的子串都不能是回文串了
while((left >= 0) && (right < s.length()) && (s.charAt(left) == s.charAt(right))){
//指针由中间向两边展开
left--;//左边界向左
right++;//右边界向右
}
//跳出循环的时候肯定是满足 s.charAt(left) != s.charAt(right)
//返回以s[left]和s[right]为中心的最长的回文子串,回文串的长度是right-left+1-2 = right - left - 1,因为跳出while循环时恰好满足s.charAt(i) != s.charAt(j)
return right - left - 1;
}
}
然后呢,还有一位大神以及官方答案,参考记录如下:
/**
* Copyright (c) 2013-Now http://AIminminAI.com All rights reserved.
*/
package DynamicProgramming;
/**
* 为什么咱们会选用动态规划DP呢,原因如下:
* 咱们拿到这个题得先分析一下,这个题有没有层层脱皮性质与层层套皮性质
* 对于一个子串而言,如果这个子串是回文串并且长度大于2,那么层层脱皮性质就是将这个字符串首尾两个字母去除之后剩下的中间这部分还是个回文字符串,那么原来的这个字符串肯定是个回文字符串
* 为什么要长度大于2,你长度为1就一个字符,你玩呢???
* 你长度为0,官话把你给判空了,你还玩啥
* 对于一个子串,如果长度大于2而且是个回文字符串,首尾各加上一个一个相同的字符依旧是回文字符串,这是层层套皮性质
* 满足这俩性质后我们可以确定这个题就可以用动态规划DP方法解决本题
*
* @author HHB
* @version 2022年4月17日
*/
public class LongestPalindromicSubstring {
public String longestPalindrome(String str){
if(s.length() == 0 || s == null || s.equals("")){
return str;
}
/**
*
* 这一步也相当于官话判空
*/
if(str.length() < 2){
return str;
}
//str[leftBound.....rightBound]表示字符串str的第leftBound个字符到第rightBound个字符组成的串是否为回文字符串,如果是则dp[leftBound][rightBound] = true;
//dp[leftBound][rightBound]代表str[leftBound.....rightBound]是否为回文字符串,是个布尔变量哦
//刚初始化dp是这个样子:[[false, false, false, false, false], [false, false, false, false, false], [false, false, false, false, false], [false, false, false, false, false], [false, false, false, false, false]]
boolean[][] dp = new boolean[str.length()][str.length()];
//初始化:所有长度为1的子串都是回文字符串呀
//业界不也是经常会填一个dp表格嘛,这个其实也就是在填dp表格的对角线,因为对角线上的元素肯定是回文的,dp是这个样子:[[true, false, false, false, false], [false, true, false, false, false], [false, false, true, false, false], [false, false, false, true, false], [false, false, false, false, true]]
for (int i = 0; i < str.length(); i++) {
dp[i][i] = true;
}
int maxLengthResult = 1;
int begin = 0;
//原来str是这样子:babad,经过这一步就变成这样了:[b, a, b, a, d],这就是chars的样子哦,惊喜不
char[] chars = str.toCharArray();
//递推开始,两个for循环整起
//先枚举子串长度,strLen就代表咱们每次枚举的子串的长度,也就是相当于一个间距,相当于先把你要枚举的串长度掐定然后你两个指针在按照这个间距去移动遍历吧
for (int strLen = 2; strLen <= str.length(); strLen++) {
//枚举左边界,左边界的上限设置可以宽松一点
/**
* 第一轮循环leftBound=0,rightBound=1,相当于开始比较str[0...1]内的字符串是不是回文字符串
* 第二轮循环leftBound+1,leftBound=1,rightBound经计算变为2,进入else{}
* 第三轮循环leftBound=2,rightBound=3
* 第四轮循环leftBound=3,rightBound=4
* 第五轮循环leftBound=4,rightBound=5
*
*
* 先是01,12,23,34,45,接着是02,13,24,35,接着是03,14,25,接着是04,
*/
for (int leftBound = 0; leftBound < str.length(); leftBound++) {
//由strLen和leftBound可以确定判定范围内的字符的右边界(右边界就是strLen + leftBound - 1),也相当于目标字符串,或者说回文字符串
int rightBound = strLen + leftBound - 1;
// 如果右边界越界,那还判断个啥,都越界了,就可以退出当前循环
if(rightBound >= str.length()){
break;
}
if(chars[leftBound] != chars[rightBound]){
dp[leftBound][rightBound] = false;
}else {
/**
* 这个rightBound - leftBound < 3是怎么来的呢。是这样,如果判断走到if这里证明字符串头尾两个字符是相等的,那么按照动态规划的思想,我们去掉头尾两个字符,
* 中间剩下的字符串依旧是回文字符串,那中间剩下的字符串的长度不就是rightBound - 1 - (leftBound + 1) + 1 < 2,因为一个字符的咱们在上面已经判断过了呀,
* 你整理一下不就是rightBound - leftBound < 3嘛
*/
if(rightBound - leftBound < 3){
dp[leftBound][rightBound] = true;
}else {
dp[leftBound][rightBound] = dp[leftBound + 1][rightBound - 1];
}
}
//只要dp[leftBound][rightBound] == true就表示子串str[leftBound.....rightBound]是回文字符串,此时咱们就得记录下来回文串的长度和起始位置
if (dp[leftBound][rightBound] && rightBound - leftBound + 1 > maxLengthResult) {
maxLengthResult = rightBound - leftBound + 1;
begin = leftBound;
}
}
}
return str.substring(begin, begin + maxLengthResult);
}
//public static void main(String[] args) {
//String str = "babad";
//String longestPalindrome = new LongestPalindromicSubstring().longestPalindrome(str);
//System.out.println(longestPalindrome);
//}
}
2.在字符串中找找有多少个回文子串
class Solution {
public int countSubstrings(String s) {
//时间复杂度为O(N^2),最坏情况下两个指针要把整个字符串走两遍哦,空间复杂度为O(1)
//官话判空
if(s == null || s.length() == 0){
return 0;
}
int result = 0;
for(int i = 0; i < 2 * s.length() - 1; i++){
int left = i / 2;
int right = i / 2 + i % 2;
//如果两边的字母相同,也就是s.charAt(left) == s.charAt(right),我们就可以继续扩展。如果两边的字母不同,我们就可以停止扩展,因为在这之后的子串都不能是回文串了
while((left >= 0) && (right < s.length()) && (s.charAt(left) == s.charAt(right))){
//指针由中间向两边展开
left--;
right++;
result++;
}
}
return result;
}
}
3.判断一个单链表是不是回文链表
//判断一个字符串是不是回文串就不需要考虑奇偶情况,只需要双指针技巧,从两端向中间逼近
boolean isPalindrome(String s){
int left = 0;
int right = s.length - 1;
while(left < right){
if(s[left] != s[right]){
return false;
}
left++;
right--;
}
return true;
}
判断一个单链表是不是回文链表
思路就是把原始链表反转存于一条新的链表中,然后比较这两条链表是否相同。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
ListNode left;
public boolean isPalindrome(ListNode head) {
//左侧指针
left = head;
return traverse(left);
}
public boolean traverse(ListNode right){
if(right == null){
return true;
}
//迭代,一步两步
boolean result = traverse(right.next);
result = result && (right.val == left.val);
//或者这样写能更明白一点:
//boolean result = (traverse(right.next)) && (right.val == left.val);
left = left.next;
return result;
}
}
其中部分代码是借鉴反转整个链表:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
//递归函数的base case,也相当于链表的官话判空
if(head == null){
return null;
}
if(head.next == null){
return head;//相当于链表只有一个节点时再怎么反转也是他自己,就直接返回自己即可
}
ListNode last = reverseList(head.next);
head.next.next = head;//反转链表节点之间的指针
head.next = null;//当链表递归反转之后,新的头节点变成了last,head变成了新链表的最后一个节点,别忘了链表的末尾指针要指向null,所以才有z了这一句代码
return last;
}
}
在题库的解题答案中看到有位大神把反转链表泛化了一下,现在不是反转整个链表嘛,这个大神整理的是反转以dog开头的节点或者说以dog为头节点的链表,或者最准确的说法是反转dog节点到最后一个null之间的节点。
//把这个dog换成head就是反转单链表的程序
ListNode reverseDogToLast(ListNode dog){
ListNode prev = null;//代表当前节点的前一个节点
ListNode cur = dog;//代表当前节点
ListNode next = dog;//代表当前节点的下一个节点
while(cur != null){
next = cur.next;
cur.next = prev;//当前节点的指针指向自己的前一个节点prev,不就和咱们那个head.next.next = head是一个道理的。
prev = cur;//步进前一个节点
cur = next;//步进当前节点
}
return prev;
}
再泛化一下,我不反转dog节点到最后一个null之间的节点,反转dog节点到cat节点之间的链表那一段
就是把上面dog到null的终止条件改一下就行了呗
ListNode reverseDogToCat(ListNode dog, ListNode cat){
ListNode prev = null;
ListNode cur = dog;
ListNode next = dog;
//改一下while处的终止条件
while(cur != cat){
next = cur.next;
cur.next = prev;//当前节点的指针指向自己的前一个节点prev,不就和咱们那个head.next.next = head是一个道理的。
prev = cur;//步进前一个节点
cur = next;//步进当前节点
}
return prev;
}
当然啦,反转链表也有很多相类似的体型咯
先用一个for循环找到第m个位置,然后再用一个for循环将m和n之间的元素反转。迭代法的空间复杂度为O(1)
//也就是反转以head开头的或者说以head为起点的n个节点,并返回新的头节点
public ListNode reverseOneToN(ListNode head, int n){
//if(n == 1){...}
//或者if(head.next == null){...}一个道理,都是说明链表中若只有一个节点,你咋玩,只能return head喽
ListNode nodeNPlusOne = null;
if(head.next == null){
nodeNPlusOne = head.next;
return head;
}
//那就成了。以head.next为起点需要反转前n - 1 个节点
ListNode last = reverseOneToN(head.next, n - 1);//咱们删除倒数第K个节点时这里也用的是这样的迭代,只不过那个返回的是第n + 1个节点,也就是后一个节点,方便咱们删除,但是0到N和1到n+1不就是一个道理嘛。
head.next.next = nodeNPlusOne;//
return last
}
ListNode reverseKGroup(ListNode head, int k){
//先官话判空呗
if(head == null){
return null;//空的再反转他还是空的
}
ListNode point1 = head;
ListNode point2 = head;
for(int i = 0; i < k; i++){//觉不觉得熟悉,咱们找链表倒数第K个节点时以及找到链表倒数第K个节点并删除并返回头节点的那两道题中也用了这个for循环哟
if(point2 == null){
return head;//由point1和point2组成一个[point1, point2)区间,如果此时区间内元素不足K个,直接返回head,都不符合条件我反转个啥
}
point2 = point2.next;//区间右边界步进,像不像那个滑动窗口呀
}
//反转前K个元素
ListNode firstLevelHead = reverseLeftToRight(point1, point2);
//递归反转后,将小链表们串起来
point1.next = reverseKGroup(point2, k);
return firstLevelHead;
}