题目
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-palindromic-substring
代码及分析
方法一:中心扩散法
回文串即为正反序相同的字符串。
我们可以通过中心扩散的方法判断一个子串是否为回文串,但需要注意的一点是当子串的字符个数奇偶不同时,其对应的中心位置有所不同。例如题中所给的示例:bab为奇数,其中心位置为a,而bb为偶数,其对应位置为bb中间。
建立一个函数,用于对字符串中心扩散,需要两个指针,分别向左右两边进行扩散,当字符个数为偶数时,左右两边起始位置不同,当字符个数为奇数时,左右两边起始位置相同。
为了方便统计更新最长回文子串的起始位置和长度,可以采用引用的方法。
string longestPalindrome(string s) {
int l = s.length();
if(l < 2 )
return s;
int len = 0; //最长回文子串的长度
int begin = 0; //最长回文子串开始位置
for(int i = 0; i < l-1; i++)
{
longest_string(s, i, i, begin, len); // 对应于偶数的情况
longest_string(s, i, i+1, begin, len); // 对应于奇数的情况
}
return s.substr(begin,len);
}
void longest_string(string s, int left, int right, int &begin, int &len)
{
while(left >= 0 && right <= s.length() && s[left] == s[right])
{
left--;
right++;
}
left = left+1;
right = right-1;
if(len < (right-left+1))
{
begin = left;
len = right-left+1;
}
}
复杂度:
时间复杂度: O(n^2) 遍历整个数组需要O(n)的时间,调用的中心扩散函数需要O(n)的时间故,共需要O(n ^ 2)。
空间复杂度: O(1) 。
补充:当在对数组进行遍历的时候,若当前指针i之后的长度小于当前最大长度len的一半时,后续变没有再继续遍历的必要,故我们可以在遍历时增加条件:
for(int i = 0; i < l-1; i++)
{
if((l-i-1) < len/2)
break;
longest_string(s, i, i, begin, len);
longest_string(s, i, i+1, begin, len);
}
方法二:动态规划法
该问题也可以用动态规划法进行求解。
建立一个二维的dp数组,其中dp[i][j]表示字符串中i到j是否为回文子串,是则为1, 反之为0;
显然,应该有dp[i][i]=1,当j < i 时不满足条件,无需计算;
对于dp[i][j],当j-i < 2时, 说明 有 i == j 或者 i 和 j 相邻,此时,只需判断 s[i] == s[j];
当j-i > 2时, 除了需要判断s[i] == s[j] 之外,还需判断dp[i+1][j-1] 是否为1。
另外,需要注意的一点是,因为在最后一种情况中,dp[i][j] 的值需要用到dp[i+1][j-1], 故我们不能按照 i 递增的顺序对字符串进行遍历,可以采用 i 递减的顺序进行遍历。
string longestPalindrome(string s) {
int l = s.length();
if(l < 2)
return s;
int begin;
int len = 0;
vector<vector<int>> dp(l,vector<int>(l,0));
for(int i = 0; i < l; i ++) dp[i][i] = 1;
for(int i = l-1; i >= 0; i--)
{
for(int j = i; j < l; j++)
{
if((j-i) < 2) dp[i][j] = (s[i] == s[j]);
else if(s[i] == s[j])
dp[i][j] = dp[i+1][j-1];
if(dp[i][j] && (j-i+1) > len)
{
len = j-i+1;
begin = i;
}
}
}
return s.substr(begin,len);
}
复杂度:
时间复杂度: O(n^2) ;
空间复杂度: O(n^2) 。
方法三:Manacher’s Algorithm 马拉车算法
该问题有一个比较经典的解法,Manacher’s Algorithm 马拉车算法。
这个算法理解起来比较复杂,但代码还是很简单的。
string longestPalindrome(string s) {
int l = s.length();
if(l < 2)
return s;
// 为数组插入字符"#"
string str = "#";
for(int i = 0; i < l; i ++)
{
str += s[i];
str += "#";
}
int len = str.length();
vector<int> p(len,0);
int md = 0, id = 0;
int maxlen = 0, maxcenter = 0;
for(int i = 0; i < len; i ++)
{
p[i] = md > i ? min(p[2*id-i], md-i) : 1;
while(i+p[i]<len && i-p[i]>=0&& str[i+p[i]] == str[i-p[i]]) ++p[i];
if(p[i] >= maxlen)
{
maxlen = p[i];
maxcenter = i;
}
if(md < i+p[i]-1)
{
md = i+p[i]-1;
id = i;
}
}
return s.substr((maxcenter+1-maxlen)/2,maxlen-1);
}
复杂度:
时间复杂度: O(n) ;
空间复杂度: O(n) 。
注:我学习这个算法主要参考了leetcode中题解部分的 https://leetcode-cn.com/problems/two-sum/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/
以及 https://www.cnblogs.com/grandyang/p/4475985.htm
我觉得这两篇讲解的非常详细,非常值得学习。