题目描述
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba"也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
解决思路 1 —— 暴力枚举法
暴力枚举法是这几种方法中最直观的求解,
求解过程中分别以每个元素为中间元素(奇数为最中间的一个数,偶数为中间元素的其中一个),
同时从左右出发,计算出最长的回文子串。
class Solution {
public:
string longestPalindrome(string s) {
int startIndex = 0;
int endIndex = 0;
// 保存最大回文子串的起始与终点位置
int resStart = 0;
int resEnd = 0;
int maxLen = 0;
int countLen = 0;
for(int i = 0; i < s.size(); i++) {
/* 回文子串为奇串处理 */
startIndex = i;
endIndex = i;
countLen = 0;
while(startIndex >= 0 && endIndex < s.size()) {
if(s[startIndex] == s[endIndex]) {
if(startIndex == endIndex) {
countLen++;
} else {
countLen += 2;
}
startIndex--;
endIndex++;
} else {
break;
}
}
// 当前回文子串大于上一次最大回文子串
if(countLen > maxLen) {
maxLen = countLen;
resStart = startIndex + 1;
resEnd = endIndex - 1;
}
/* 回文子串为奇串处理 */
startIndex = i-1;
endIndex = i;
countLen = 0;
while(startIndex >= 0 && endIndex < s.size()) {
if(s[startIndex] == s[endIndex]) {
if(startIndex == endIndex) {
countLen++;
} else {
countLen += 2;
}
startIndex--;
endIndex++;
} else {
break;
}
}
// 当前回文子串大于上一次最大回文子串
if(countLen > maxLen) {
maxLen = countLen;
resStart = startIndex + 1;
resEnd = endIndex - 1;
}
}
return s.substr(resStart, resEnd - resStart + 1);
}
};
class Solution {
public:
string longestPalindrome(string s) {
string res="";//存放结果
string temp="";//存放子串
for(int i=0;i<s.length();i++)
{
for(int j=i;j<s.length();j++)
{
temp=temp+s[j];
string tem=temp;//tem存放子串反转结果
std::reverse(tem.begin(),tem.end());//反转
if(temp==tem)
res=res.length()>temp.length()?res:temp;
}
temp="";
}
return res;
}
};
复杂度分析
- 时间复杂度:最外层循环复杂度为O(n),内层两个循环的复杂度都为O(n/2),因此时间复杂度为O(n^2)。
- 空间复杂度:在此算法中,没有使用额外的辅助空间,因此空间复杂度为O(1)。
解决思路 2 —— 记忆化搜索
回文字符串的子串也是回文,我们可以将最长回文子串分解一系列子问题,使用动态规划求解。
设状态f(i,j)表示区间[i,j]是否为回文串,
f(i,j) = false表示子串[i,j]不是回文,f(i,j)=true表示子串[i,j]是回文串,则有以下的关系:
class Solution{
public:
string longestPalindrome(string s) {
int n;
n = s.size();
bool **f = new bool*[n];
for(int i = 0; i < n; i++) {
f[i] = new bool[n];
}
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
f[i][j] = false;
}
}
int maxLen = 1;
int start = 0;
for(int i = 0; i < s.size(); i++) {
f[i][i] = true;
for(int j = 0; j < i; j++) {
f[j][i] = (s[j] == s[i] && (i-j < 2 || f[j+1][i-1]));
if(f[j][i] && maxLen < (i-j+1)) {
maxLen = i - j + 1;
start = j;
}
}
}
for(int i = 0; i < n; i++) {
delete[] f[i];
}
delete[] f;
return s.substr(start, maxLen);
}
};
class Solution {
public:
string longestPalindrome(string s) {
int len=s.size();
if(len==0||len==1)
return s;
int start=0;//回文串起始位置
int max=1;//回文串最大长度
vector<vector<int>> dp(len,vector<int>(len));//定义二维动态数组
for(int i=0;i<len;i++)//初始化状态
{
dp[i][i]=1;
if(i<len-1&&s[i]==s[i+1])
{
dp[i][i+1]=1;
max=2;
start=i;
}
}
for(int l=3;l<=len;l++)//l表示检索的子串长度,等于3表示先检索长度为3的子串
{
for(int i=0;i+l-1<len;i++)
{
int j=l+i-1;//终止字符位置
if(s[i]==s[j]&&dp[i+1][j-1]==1)//状态转移
{
dp[i][j]=1;
start=i;
max=l;
}
}
}
return s.substr(start,max);//获取最长回文子串
}
};
复杂度分析
- 时间复杂度:使用了两层循环,因此时间复杂度为O(n2)。
- 空间复杂度:使用了f[n][n]作为辅助空间,因此空间复杂度为O(n2)。
解决思路 3 —— Mancher’s Algorithm
Mancher算法能够很快的得到一个字符串中以任意一个字符为中心的回文子串,
其基本原理使用已知回文串的左半部分来推导有半部分。
我们使用rad[i]表示以第i个字符为中心的最长回文半径,
假设我们求出了rad[0,…,i-1]的值,现在我们需要通过已知的结果计算出rad[i]的值,
在此我们定义maxRight是i位置前所有回文串能延伸到的最右端位置,并且此时回文串的中心位置为k(取第一个达到最右端的位置),
则我们可以得到maxRight = k + rad[k] ( 中心位置加上半径 ),可以有以下两种情况:
第一种情况:位置i不在前面的任何回文串中,即i > maxRight,这时则初始化rad[i]=1,然后rad[i]向两边延伸,即
while(s[i+rad[i]] == s[i-rad[i]])
rad[i]++;
第二种情况:i这个位置被前面位置k为中心的回文串包含,即i <= maxRight,这种情况下rad[i]就不是从1开始。由回文串的性质,我们可以知道2k-i这个位置与i关于k对称,在这种情况下由可分为三种情形:
第一种情形:以2k-i为中心的回文串有一部分在以i为中心回文串之外,
这种情况下rad[i]=maxRight-i=k+rad[k]-i,即为图中空心箭头长度,
那有没有可能rad[i]会更长呢?不可能,如果rad[i]会更长,则会延伸到k为中心的子串外,
由于i和2k-i的对称性可得到k为中心的子串会大于图中紫色对应的半径,与已知矛盾。
第二种情形:以2k-i为中心的回文串在以i为中心回文串之内,此时rad[i]=rad[2k-i],
那么这个时候rad[i]会更长吗?不可能,如果rad[i]长度更长,则延伸部分与2k-i正好对称,
这个时候2k-i子串半径则大于图中蓝色箭头的长度,矛盾。
第三种情形:以2k-i为中心的回文串与i为中心回文串左端部重叠,则ran[i]=rad[2k-i],并且rad[i]可能继续增加,所以有:
rad[i]=rad[2k-i];
while(s[i+rad[i]] == s[i-rad[i]])
rad[i]++;
上面的方法存在一个问题,就是只能计算出奇数长度的回文子串,偶数的就不行,
怎么解决呢?这里使用一个比较好的方法,在原来的串中每两个字符之间添加一个特殊字符,
如aabbaca变成 ^#a#a#b#b#a#c#a#$ ,这里 ^$ 作为字符串的定界符是为了防止算法越界。
这样处理后,无论原来的回文子串长度是偶数还是奇数,现在都变成奇数了。
class Solution{
public:
// 字符串预处理,如输入"abba",返回"^#a#b#b#a#$"
string preProcess(string s) {
int n = s.size();
if(n == 0)
return "^$";
string ret = "^";
for(int i = 0; i < n; i++)
ret += "#" + s.substr(i,1);
ret+= "#$";
return ret;
}
string longestPalindrome(string s) {
string tmpStr = preProcess(s);
int n = tmpStr.size();
int *rad = new int[n];
int center = 0, maxRight = 0;
for(int i = 1; i < n - 1; i++) {
int sym_pos = 2 * center - i;
rad[i] = (maxRight > i) ? min(maxRight - i, rad[sym_pos]) : 1;
// 以i为中心检索两边的字符串
while(tmpStr[i + rad[i]] == tmpStr[i - rad[i]])
rad[i]++;
if(i + rad[i] > maxRight) {
center = i;
maxRight = i + rad[i];
}
}
// 寻找最长的半径
int maxLen = 0;
int center_index = 0;
for(int i = 1; i < n-1; i++) {
if(rad[i] > maxLen) {
maxLen = rad[i];
center_index = i;
}
}
return s.substr((center_index - maxLen) / 2, maxLen-1);
}
};
复杂度分析
- 时间复杂度:Mancher算法时间复杂度为O(n)。
- 空间复杂度:空间复杂度也为O(n)。
- 参考链接
- 作者:gpe3DBjDS1
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-c-by-gpe3dbjds1/