题目:给定一个字符串 s,找到 s 中最长的回文子串。假设 s 的最大长度为 1000。
eg:输入: “babad” 输出: “bab” 注意: “aba” 也是一个有效答案。
输入: “cbbd” 输出: “bb”
回文串就是一个正读和反读都一样的字符串
答案:
方法一:最长公共子串。
反转 S,使之变成 S’。找到 S和 S’之间最长的公共子串,这也必然是最长的回文子串。
但是这个方法有bug 需要纠正。S=“abacdfgdcaba” , S’ =“abacdgfdcaba”:S 以及 S’之间的最长公共子串为 “abacd”,显然,这不是回文。我们可以看到,当 SS 的其他部分中存在非回文子串的反向副本时,最长公共子串法就会失败。为了纠正这一点,每当我们找到最长的公共子串的候选项时,都需要检查子串的索引是否与反向子串的原始索引相同。如果相同,那么我们尝试更新目前为止找到的最长回文子串;如果不是,我们就跳过这个候选项并继续寻找下一个候选。
时间复杂度O(n2)
空间复杂度O(n2)
方法二:暴力法
遍历所有子字符串,判断是否是回文,并返回最大长度。
class Solution{
public String longestPalindrome(String s) {
int n = s.length();
int i,j,start=0,end=0,max=-1,flag=0;
if(s == null || n == 0) return "";
for(i=0;i<n;i++)
{
for(j=i;j<n;j++)
{
start = i;
end = j;
while(start <= end)
{
if(s.charAt(start) == s.charAt(end))
{
start++;
end--;
}
else break;
}
//首尾相遇则为回文
if(start > end)
{
if(j-i > max)
{
max = j-i;
flag = i;
}
}
}
}
return s.substring(flag,flag+max+1);
}
}
时间复杂度O(n3)
空间复杂度O(1)
假设输入:abcddcba,按照上述程序,要分割成 'abcddcba’, 'bcddcb’, 'cddc’, 'dd’…等字符串,并对这些字符串分别进行判断。不难发现,很多短子字符串在长些的子字符串中比较过,这导致了大量的冗余判断,根本原因是:对字符串对称的判断是由外向里进行的。
换一种思路,从里向外来判断。也就是先判断子字符串(如dd)是不是对称的。如果它(dd)不是对称的,那么向该子字符串两端各延长一个字符得到的字符串肯定不是对称的。如果它(dd)对称,那么只需要判断它(dd)两端延长的一个字符是不是相等的,如果相等,则延长后的字符串是对称的。
方法三:中心扩展算法
class Solution{
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) return "";
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i);
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
private int expandAroundCenter(String s, int left, int right) {
int L = left, R = right;
while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
L--;
R++;
}
return R - L - 1;
}
}
时间复杂度O(n2)
空间复杂度O(1)
方法四:Manacher算法
算法的基本思路是这样的:把原串每个字符中间用一个串中没出现过的字符分隔#开来(统一奇偶),同时为了防止越界,在字符串的首部也加入一个特殊符$,但是与分隔符不同。同时字符串的末尾也加入’\0’。算法的核心:用辅助数组p记录以每个字符为核心的最长回文字符串半径。也就是p[i]记录了以str[i]为中心的最长回文字符串半径。p[i]最小为1,此时回文字符串就是字符串本身。
//预处理,将str:abba转换为: $#a#b#b#a#\0(从1开始)
char * pre(char *str)
{
int length = strlen(str);
char *prestr = new char[2*length + 4];
prestr[1] = '$';
for(int i=0;i<length;i++)
{
prestr[2*(i+1)] = '#';
prestr[2*(i+1)+1] = str[i];
}
prestr[2*length+2]='#';
prestr[2*length+3]='\0';
return prestr;
}
//manacher算法。pi记录具有遍历过程中最长半径的回文字符串中心字符串。mx记录了具有最长回文字符串的右边界。
int getMaxSym3(char *str)
{
char *prestr = pre(str);
int mx =0, pi=1;//边界和对称中心
int len = strlen(prestr);
//辅助数组
int *p = new int[len];
p[0] = 0;
for(int i=1;i<len;i++)
{
if(mx>i)
{
p[i]=min(mx-i,p[2*pi-i]);//核心
}
else
{
p[i]=1;
}
while(prestr[i-p[i]]==prestr[i+p[i]]&&i-p[i]>0&&i+p[i]<len)
{
p[i]++;
}
if(i+p[i] > mx)
{
mx = p[i] + i;
pi = i;
}
}
//最大回文字符串长度
int maxlen = 0;
for(int i=0;i<len;i++)
{
if(p[i]>maxlen)
{
maxlen = p[i];
}
}
delete []prestr;
delete []p;
return maxlen - 1;
}
需要注意的问题:
- 理清思路,最好将不同情况分隔开,不要混在一起
- char 单引号 string 双引号。 字符串不能直接比较相等,字符可以
- 有些边界问题可以加一个特殊字符辅助