题目:给定一个字符串s,找出s中的最长回文子串;
回文是指正着读和倒着读,结果一些样,比如abcba或abba。
方法:暴力法,DP法, 中心扩展法,manacher算法
1、暴力法
求出每一个子串,之后判断是不是回文,找到最长的那个。
求遍历每一个子串的方法的时间复杂度O(n^2),判断每一个子串是不是回文的时间复杂度是O(n),所以总时间复杂度为O(n^3)。
思想:
1)从最长的子串开始,遍历所有该原字符串的子串;
2)每找出一个字符串,就判断该字符串是否为回文;
3)子串为回文时,则找到了最长的回文子串,因此结束;反之,则继续遍历。
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 index1, index2;
// 对每个子串都从两边开始向中间遍历 判断是不是回文
for (index1 = i, index2 = j; index1 < index2; index1++, index2--)
{
if (s.at(index1) != s.at(index2))
break;
}
// 若index1=index2,表示串是类似于abcba这种类型;若大于,则是abccba这种类型
if (index1 >= index2 && j - i > maxlength)
{
maxlength = j - i + 1;
start = i;
}
}
}
if (maxlength > 0)
return s.substr(start, maxlength);//求子串
return NULL;
}
2、动态规划
回文字符串的子串也是回文,比如P[i,j](表示以i开始以j结束的子串)是回文字符串,那么P[i+1,j-1]也是回文字符串。这样最长回文子串就能分解成一系列子问题了。这样需要额外的空间O(n^2),算法复杂度也是O(n^2)。
首先定义状态方程和转移方程:
P[i,j]=false: 表示子串[i,j]不是回文串。
P[i,j]=true: 表示子串[i,j]是回文串。
P[i,i]=true: 当且仅当P[i+1,j-1] = true && (s[i]==s[j]) 否则p[i,j] =false;
string findLongestPalindrome(string &s)
{
const int length = s.size();
int maxlength = 0;
int start;
bool P[50][50] = { false };
// 子串长度为1和为2的初始化
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;
}
}
// 使用上述结果可以dp出子串长度为3~len -1的子串
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;
}
3、中心扩展
中心扩展就是把给定的字符串的每一个字母当做中心,向两边扩展,这样来找最长的子回文串。算法复杂度为O(n^2)。
但是要考虑两种情况:
1、长度为奇数: aba
2、长度为偶数: abba
思想:
1)将子串分为单核和双核的情况,单核即指子串长度为奇数,双核则为偶数;
2)遍历每个除最后一个位置的字符index(字符位置),单核:初始low = 初始high = index,low和high均不超过原字符串的下限和上限;判断low和high处的字符是否相等,相等则low++、high++(双核:初始high = 初始low+1 = index + 1);
3)每次low与high处的字符相等时,都将当前最长的回文子串长度与high-low+1比较。后者大时,将最长的回文子串改为low与high之间的;
4)重复执行2)、3),直至high-low+1 等于原字符串长度或者遍历到最后一个字符,取当前截取到的回文子串,该子串即为最长的回文子串。
string findLongestPalindrome(string &s)
{
const int length = s.size();
int maxlength = 0;
int start;
// 类似于aba这种情况,以i为中心向两边扩展 长度为奇数
for (int i = 0; i < length; i++)
{
int j = i - 1, k = i + 1;
while (j >= 0 && k < length&&s.at(j) == s.at(k))
{
if (k - j + 1 > maxlength)
{
maxlength = k - j + 1;
start = j;
}
j--;
k++;
}
}
// 类似于abba这种情况,以i,i+1为中心向两边扩展 长度为偶数
for (int i = 0; i < length; i++)
{
int j = i, k = i + 1;
while (j >= 0 && k < length&&s.at(j) == s.at(k))
{
if (k - j + 1 > maxlength)
{
maxlength = k - j + 1;
start = j;
}
j--;
k++;
}
}
if (maxlength > 0)
return s.substr(start, maxlength);
return NULL;
}
4、Manacher法
Manacher算法首先通过在每个字符的两边都插入一个特殊的符号(未在字符串中出现过),将所有可能的回文子串都转换成奇数。例如"aba"的两边都插入字符'#'就变成了"#a#b#a#"。为了更好处理越界问题,可以在字符串的开始和结尾加入另一个特殊字符,例如在"#a#b#a#"的开始和结尾插入字符'%'变成"%#a#b#a#%"。这个算法就是利用已有回文串的对称性来计算的,由于Manacher算法只有在遇到还未匹配的位置时才进行匹配,已经匹配过的位置不再匹配,所以对于对于字符串S的每一个位置,都只进行一次匹配,所以算法的总体复杂度为O(n)。
1.思想:
1)将原字符串S的每个字符间都插入一个永远不会在S中出现的字符(本例中用“#”表示),在S的首尾也插入该字符,使得到的新字符串S_new长度为2*S.length()+1,保证Len的长度为奇数(下例中空格不表示字符,仅美观作用);
例:S: a a b a b b a
S_new: # a # a # b # a # b # b # a #
2)根据S_new求出以每个字符为中心的最长回文子串的最右端字符距离该字符的距离,存入Len数组中,即S_new[i]—S_new[r]为S_new[i]的最长回文子串的右段(S_new[2i-r]—S_new[r]为以S_new[i]为中心的最长回文子串),Len[i] = r - i + 1;
S_new: # a # a # b # a # b # b # a #
Len: 1 2 3 2 1 4 1 4 1 2 5 2 1 2 1
Len数组性质:Len[i] - 1即为以Len[i]为中心的最长回文子串在S中的长度。在S_new中,以S_new[i]为中心的最长回文子串长度为2Len[i] - 1,由于在S_new中是在每个字符两侧都有新字符“#”,观察可知“#”的数量一定是比原字符多1的,即有Len[i]个,因此真实的回文子串长度为Len[i] - 1,最长回文子串长度为Math.max(Len) - 1。
3)Len数组求解(线性复杂度(O(n))):
a.遍历S_new数组,i为当前遍历到的位置,即求解以S_new[i]为中心的最长回文子串的Len[i];
b.设置两个参数:sub_midd = Len.indexOf(Math.max(Len)表示在i之前所得到的Len数组中的最大值所在位置、sub_side = sub_midd + Len[sub_midd] - 1表示以sub_midd为中心的最长回文子串的最右端在S_new中的位置。起始sub_midd和sub_side设为0,从S_new中的第一个字母开始计算,每次计算后都需要更新sub_midd和sub_side;
c.当i < sub_side时,取i关于sub_midd的对称点j(j = 2sub_midd - i,由于i <= sub_side,因此2sub_midd - sub_side <= j <= sub_midd);当Len[j] < sub_side - i时,即以S_new[j]为中心的最长回文子串是在以S_new[sub_midd]为中心的最长回文子串的内部,再由于i、j关于sub_midd对称,可知Len[i] = Len[j];
当Len[j] >= sub.side - i时说明以S_new[i]为中心的回文串可能延伸到sub_side之外,而大于sub_side的部分还没有进行匹配,所以要从sub_side+1位置开始进行匹配,直到匹配失败以后,从而更新sub_side和对应的sub_midd以及Len[i];
d.当i > sub_side时,则说明以S_new[i]为中心的最长回文子串还没开始匹配寻找,因此需要一个一个进行匹配寻找,结束后更新sub_side和对应的sub_midd以及Len[i]。
#define min(x, y) ((x)<(y)?(x):(y))
#define max(x, y) ((x)<(y)?(y):(x))
string findLongestPalindrome3(string s)
{
int length = s.size();
for (int i = 0, k = 1; i < length - 1; i++)//给字符串添加 #
{
s.insert(k, "#");
k = k + 2;
}
length = length * 2 - 1;//添加#后字符串长度
int *rad = new int[length]();
rad[0] = 0;
for (int i = 1, j = 1, k; i < length; i = i + k)
{
while (i - j >= 0 && i + j < length&&s.at(i - j) == s.at(i + j))
j++;
rad[i] = j - 1;
//镜像,遇到rad[i-k]=rad[i]-k停止,这时不用从j=1开始比较
for (k = 1; k <= rad[i] && rad[i - k] != rad[i] - k; k++)
rad[i + k] = min(rad[i - k], rad[i] - k);
j = max(j - k, 0);//更新j
}
int max = 0;
int center;
for (int i = 0; i < length; i++)
{
if (rad[i] > max)
{
max = rad[i];
center = i;
}
}
return s.substr(center - max, 2 * max + 1);
}