问题描述:
在一个字符串中s,找到最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
提示:
-
1 <= s.length <= 1000
-
s 仅由数字和英文字母组成
关键词1: 回文串
回文串是一种特殊的字符串,从左往右读的顺序和从右往左读的顺序是一样的。如下图。
关键词2: 找到子串
子串是在整个字符串中截取的长度不等的连续字符串。如下图。
关键词3: 最长
任务描述:
找到一个子串,该子串为回文串。并且该子串是所有回文子串中,长度最长的。
任务1:找到子串
在一整串字符中,用滑动窗口截取一段连续字符。但是,窗口的滑动方式对应了不同的时间复杂度。
滑动方法1:将窗口左端固定,滑动右端。或者将窗口右端固定,滑动左端。
由于左固右滑和右固左滑的方式一样。所以,以下只描述左固右滑的具体过程。
1、找到一个固定的左端i
2、向右移动一格,找到右端j
3、得到字符串s[i.....j]
4、重复步骤2,得到所有以i为左端的字符串
5、向右移动1格,得到新的左端i+1,重复步骤2,得到所有以i+1位左端的字符串
6、重复步骤5,得到整个子串可拆分的子串。
代码如下:
for(int i=0;i<s.size();i++)
{
for(int j=1;j<s.size()-i+1;j++)
{
string tempstr=s.substr(i,j);//用substr函数得到,以i为起点,长度为j的子串
//任务2:判断是否回文
//任务3:判断回文子串长度是否最大
}
}
滑动方法2:将窗口中心固定,向左或者向右滑动(中心扩散法)
1、找到一个固定的中心i
2、向左扩散寻找符合要求的字符。若找到,加入子串,并继续向左扩散;若未找到,则停止向左扩散寻找
3、同样,向右扩散寻找符合要求的字符。若找到,加入子串,并继续向右扩散;若未找到,则停止向右扩散
4、同时向两边扩散。如果符合要求,同时将两边字符加入子串,若不符合,则停止扩散。最终得到以i为中心的子串
5、向右移动1格中心点,得到新的中心点i+1,重复步骤2,得到以i+1为中心点的子串
6、重复步骤5,得到所有符合要求的子串
代码如下:
for(int i=0;i<n;i++) //固定i为窗口中心
{
int left=i-1; //定义一个初步的左窗口
int right=i+1; //定义一个初步的右窗口
while(left>=0&&s[left]==s[i]) //满足向左扩散的条件
{left--; //窗口左移
//其余任务的处理}
while(right<=n-1&&s[right]==s[i]) //满足向右扩散的条件
{right++; //窗口右移
//其余任务的处理}
while(left>=0&&right<=n-1&&s[left]==s[right])//满足同时左右扩散的条件
{left--;right++; //左右窗口同时移动
//其余任务的处理}
}
任务2:判断是否回文
判断一个子串是否满足回文特性。
方法1:从定义出发(即,从左往右读的顺序和从右往左读的顺序是一样的)
将字符串倒转过来,相互比对。若相同,则是回文串;如不同,则不是回文串。
该方法简单、暴力,对于所有滑动方式得到的子串都合适。
代码如下:
//通过任务1,得到了字符串
string fanzhuan=tempstr; //定义一个字符串用来存储翻转后的子串
reverse(fanzhuan.begin(),fanzhuan.end()); //翻转字符串
if(fanzhuan==tempstr) //比对翻转前后的子串
{
//若比对相同,则该字符串是回文串,然后进行任务3的处理
}
方法2:中心扩散法(以回文串s为中心,判断向两边扩散后的字符串是否同样回文)
该方法仅适用于以中心扩散为窗口滑动方式的子串。
对于已经是回文串的子串s来说,若子串前后字符均相同,则扩散后的子串肯定也是回文串。如不同,则扩散后的子串肯定不是回文串。
但是,只向两边扩散的方法,对于如下图所示的初始子串{a,b,b,a},肯定是不行的。
因为只向两边扩散的方法只会判定{b,b,a}是非回文,而忽略掉{b,b}子串。
所以,需要对中心i向左或者向右扩散,找到重复的字符,将其加入回文子串中,以防止上述情况发生。
但是,单边扩散仅限于回文中心的初始位置。因为回文中心是重复字符的话,该中心必定符合回文性质。如果重复字符出现在字符串非中心位置,用两边同时扩散的检测方法,同样可以判断出来。
1、找到一个固定的中心i
2、向左扩散寻找与s[i]相同的字符。若找到,加入子串,并继续向左扩散;直到遇到与s[i]不相同的字符
3、同样,向右扩散寻找与s[i]相同的字符。若找到,加入子串,并继续向右扩散;直到遇到与s[i]不相同的字符
4、同时向两边扩散。如果s[left]==s[right],同时将两边字符加入子串,并继续向两边扩散;直到两端字符不相同。最终得到以i为中心的子串
5、向右移动1格中心点,得到新的中心点i+1,重复步骤2,得到以i+1为中心点的子串
6、重复步骤5,得到所有符合要求的子串
//通过窗口中心的移动,得到初始的字符s[i]
//以下是判断,以s[i]为中心的字符串是否是回文,的条件
while(left>=0&&s[left]==s[i]) //若左端left与i的字符相同,且滑动窗口未超出左边界
{
left--; //窗口左移
//任务3的处理
}
while(right<=n-1&&s[right]==s[i]) //若右端right与i的字符相同,且滑动窗口未超出右边界
{
right++; //窗口右移
//任务3的处理
}
while(left>=0&&right<=n-1&&s[left]==s[right])//若左右两端的字符相同,且两端窗口未超出边界
{
left--;right++;//同时向两边扩散
//进行任务3的处理
}
任务3:判断长度是否最长
在得到一个回文子串后,需要判断该子串是否是长度最长的子串。若是,则返回该子串;若不是,则跳过。
维护两个全局变量,maxstring和maxlen,记录最长子串和其长度。
每次得到回文子串,计算长度。若长度超过maxlen,则更新maxstring和maxlen;若未超过,则跳过,寻找下一个子串。
代码如下:
//通过任务1、2,得到回文子串
strlen=tempstr.size(); //得到该子串的长度
if(maxstrlen<strlen) //若该长度大于已经被记录的最长子串长度
{
maxstring = tempstr; //更新最长子串
maxstrlen=strlen; //更新最长子串长度
}
//若未超过记录的最长子串,则跳过
完整的处理过程和代码展示如下:
(以中心扩散法为例)
class Solution {
public:
string longestPalindrome(string s) {
int n=s.size();//n为字符串长度
int maxlen=0;
int maxstr=0;//最长回文子串的起始位置
for(int i=0;i<n;i++)
{
int left=i-1;
int right=i+1;
int len=1;//以i为中心扩散时的子串长度
while(left>=0&&s[left]==s[i]) {left--;len++;}
while(right<=n-1&&s[right]==s[i]) {right++;len++;}
while(left>=0&&right<=n-1&&s[left]==s[right])
{left--;right++;len+=2;}
if(maxlen<len)
{maxlen=len;maxstr=left+1;}
}
string maxstring;
maxstring=s.substr(maxstr,maxlen);
return maxstring;
}
};