摘要
这篇文章是针对中级水平的读者而写的。主要内容介绍了:回文、动态规划、字符串操作。读这篇文章,首先确保你知道什么是回文。回文就是从不懂的方向读的结果都是一样的,比如说"aba"是一个回文,但是"abc"就不是。
解决方法
方法1 (寻找最长的公共子字符串)【可行】
容易犯的错误
我们总尝试捷径快速的解决问题,但是很不幸,不太容易(不过只要稍加修改就可以得到正确的答案)
把一个字符串S反转得到字符串S',将S和S'中最长的公共部分找出来,那么这就是所求的字符串S中最长回文子字符串。
看起来似乎确实可行,举个例子试试。比方说:
S = "caba",那么S‘ = "abac",很明显,S和S'最长的公共部分是"aba",这就是我们需要的答案。
不过别急,再来看一个例子,这个例子是这样的:
S = "abacdfgdcaba" 那么S' = "abacdgfdcaba",那么这样的话最长的公共部分就是"abacd",哎呦,似乎这个方法不太好用了,这公共部分明显不是回文啊。
算法:
我们可以看到,当反转的字符串中有与源字符串的一部分相同的时候,并不一定整个公共部分都是回文,这时候这种方法就会出错。为了纠正这个可能发生的错误,我们找出一个公共字符串作为回文的一个候选,就检验一次,检验这个公共子字符串的索引位置是否与这个公共字符串翻转前的原索引是相同的,如果是,那么这就符合我们寻找的回文,如果不是,那么跳过这个公共字符串继续寻找下一个可以作为回文候选的公共字符串,然后再验证,如此反复。
这样做的复杂度为O(n2),空间复杂度为O(n2)(可以优化到O(n)),更多关于”最长公共子字符串“的内容请看这里。
方法2 (暴力法)【时间超时】
显然,最暴力的就是把字符串的所有子字符串获取到,然后再检验是否是回文字符串。
string findLongestPalindrome(string &s)
{
int length=s.size();//字符串长度
int maxlength=0;//最长回文字符串长度
int start;//最长回文字符串起始地址
for(int i=0;i<length;i++)//起始地址
for(int j=i+1;j<length;j++)//结束地址
{
int tmp1,tmp2;
for(tmp1=i,tmp2=j;tmp1<tmp2;tmp1++,tmp2--)//判断是不是回文
{
if(s.at(tmp1)!=s.at(tmp2))
break;
}
if(tmp1>=tmp2&&j-i>maxlength)
{
maxlength=j-i+1;
start=i;
}
}
if(maxlength>0)
return s.substr(start,maxlength);//求子串
return NULL;
}
复杂度分析:
时间复杂度:O(n3),假设需要求解的字符串的长度是n,那么它总共就有(2n)=2n(n−1)个子字符串(我们默认排除了单个字符是回文的情况)。而每个子字符串的验证需要花费 的时间,所以时间复杂度是O(n3)。
空间复杂度:O(1)。
方法3 (动态规划)【可行】
为了改善暴力法的超低效率,我们来观察一下怎么样做才能够避免在验证子字符串是否是回文时的一些不必要的重复计算。假设一种情况"ababa",如果我们知道了"bab"是回文,那么"ababa"肯定是一个回文了,因为在它两边都是相同的字符"a"。
我们定义P(i,j ) 如下:
P(i,j)={true,false,if the substring Si…Sj is a palindromeotherwise.
(译者注:P(i,j) = true 表示的是子字符串S[i,j]是回文,反之为false则S[i,j]不是回文)
因此,
P(i,j )=(P(i+1,j−1) and Si==Sj)
基本情况是(译者注:也就是两个字符的回文,例如"bb"):
P(i,i )=true
P(i,i+1)=(Si==Si+1)
这其实就是在直接使用动态规划算法,我们首先只需要初始化一个和两个字母的这种回文,然后用我们上面的这个方法找到所有三个字母的回文,并一直这样,一直找到最长的回文。
string findLongestPalindrome(string &s)
{
const int length=s.size();
int maxlength=0;
int start;
bool P[50][50]={false};
for(int i=0;i<length;i++)//初始化准备
{
P[i][i]=true;
if(i<length-1&&s.at(i)==s.at(i+1))
{
P[i][i+1]=true;
start=i;
maxlength=2;
}
}
for(int len=3;len<length;len++)//子串长度
for(int i=0;i<=length-len;i++)//子串起始地址
{
int j=i+len-1;//子串结束地址
if(P[i+1][j-1]&&s.at(i)==s.at(j))
{
P[i][j]=true;
maxlength=len;
start=i;
}
}
if(maxlength>=2)
return s.substr(start,maxlength);
return NULL;
}
复杂度分析:
时间复杂度: .
空间复杂度:O(n2).需要用空间来储存表。
附加思考练习:
你是否能将上述算法的空间复杂度进行优化?
方法4(扩展中心)【可用】
事实上,我们可以在O(n2)恒定的空间内用的时间解决这个问题。
我们可以看出在回文的中心,回文呈现的是一个反映!因此,只需要2n-1个这样中心就可以对回文进行中心扩展。
你可能会问为什么是2n-1而不是n个中心?这是因为回文的中心有可能是在两个字母之间,这样的回文有偶数个字符,比如说"abba",它的中心就在两个"b"中间。
public String longestPalindrome(String s) {
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(n),整体的时间复杂度为O(n2)
空间复杂度:O(1)
方法5(Manacher算法)【可用】
其实甚至有个复杂度的算法叫做Manacher,这里有详细的解释。然而这是一个非常不同寻常的算法,没人希望你在45分钟的编码时间当中想到这个算法,不过,请继续学习了解它,我保证很有趣。