最长回文子串
问题描述
给定一个字符串,求它的最长回文子串的长度
方法1-动态规划
/**动态规划算法 **/
char* longestPalindrome(char* str)
{
int start=0;
int end=1;
int mark_start;
bool dp[strlen(str)-1][strlen(str)-1];
for(start=0;start<strlen(str)-1;start++)
for(end=1;end<strlen(str);end++)
dp[start][end]=false;
int max_len = -1;
//bool flag = false;
for(end=0; end<strlen(str)-1;end++)
{
dp[end][end]=true;
for(start=0;start<end;start++)
{
dp[start][end]=(dp[start+1][end-1]&&(str[start]==str[end])) || (str[start]==str[end]&&start+1>end-1);
if(dp[start][end]==true && max_len<end-start+1)
{
max_len = end-start+1;
mark_start = start;
}
}
}
char* result = (char*)malloc((max_len+1)*sizeof(char));
int i;
int m = 0;
for( i=mark_start;i<mark_start+max_len;i++ )
result[m++] = str[i];
result[m] = '\0';
return result;
}
方法2-最长优先遍历
算法思想
针对这个问题,笔者最先想到的方法是以最长长度(即字符串本身长度)开始,从头到尾遍历以长度为单位遍历字符串,一旦不符合回文匹配,则跳出字符串遍历。继续走最外层循环–即将遍历长度逐次减一,直到遍历长度减到2为止,若找到,返回最外层循环长度,若没找到,则返回0,程序结束。
复杂度分析
本算法最好的情况就是,一开始这个字符串就是回文,则最外层循环只进行了一次,内层循环走了n/2次,时间复杂度为O(n)
最坏的情况就是,最长子回文出现在字符串最后两位,外层循环走了n-2次,内层循环走了n-1次,时间复杂度为O(n^2)
##最长优先遍历代码实现
/***********************
Author:tmw
date:2017-11-14
************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int longest_sub_Palindrome_travel(char* array)
{
//字符串合法性检查
if(strlen(array)==0||array==NULL)
return 0;
int len,array_len;
len=array_len=strlen(array);
for( len = strlen(array);len>1;len--)//外层循环:回文字串从最长开始找起
{
//当前子回文串长度为len
int i=0;
int j=len-1;
while(j<array_len)//以len为跨度找是否有匹配的回文
{
int ii=i;//ii和jj用来保护当前游标位置,防止因进入while循环打乱
int jj=j;
while(ii<jj)
{
if(array[ii]!=array[jj])
break;
jj--;
ii++;
}
if((ii==jj)||(ii==jj+1))//奇数情况或者偶数情况找到回文,并返回长度
{
return len;//因为len从大到小,一旦找到,一定是最大的
break;
}
i++;
j++;
}
}
return 0;//跳出for循环,此时len一定为1,说明没找到
}
最长优先遍历代码测试
int main()
{
printf("测试程序\n");
char a1[]="";
printf("%s,最长回文数为 %d\n",a1,longest_sub_Palindrome_travel(a1));
char a2[]=" ";
printf("%s,最长回文数为 %d\n",a2,longest_sub_Palindrome_travel(a2));
char a3[]="42243323454454356";
printf("%s,最长回文数为 %d\n",a3,longest_sub_Palindrome_travel(a3));
char a4[]="67982232298080";
printf("%s,最长回文数为 %d\n",a4,longest_sub_Palindrome_travel(a4));
char a5[]="231319009009";
printf("%s,最长回文数为 %d\n",a5,longest_sub_Palindrome_travel(a5));
char a6[]="abba";
printf("%s,最长回文数为 %d\n",a6,longest_sub_Palindrome_travel(a6));
char a7[]="sjdflkewjiofsdfew";
printf("%s,最长回文数为 %d\n",a7,longest_sub_Palindrome_travel(a7));
return 0;
}
##最长优先遍历测试结果
方法3-中心扩展法
算法思想
根据回文字串的特征,可以从回文的“中心”出发,以“中心”向两边逐渐扩展,然后枚举中心位置,记录并更新最长回文子串长度。
算法大致会有两层循环,外层大循环是枚举中心位置,针对回文的“中心”会因子串为偶数或为奇数而不同,因此内层循环会分别对子回文为偶数或为奇数进行判断,最终返回两者找到的回文子串的最大值,算法结束。
算法复杂度O(n^2)
中心扩展法代码实现
/***********************
Author:tmw
date:2017-11-14
************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int longest_sub_Palindrome_centrol_expand(char* array)
{
//字符串合法性检查
if( strlen(array) == 0 || array == NULL )
return 0;
int i,j,max_len,len_odd,len_even;//len_odd为奇数子回文串返回的长度,len_even为偶数回文子串返回的长度
int array_len = strlen(array);
len_odd = 0;
len_even = 0;
max_len=0;
for( i = 1 ; i < array_len; i++ )//以i位置为中心
{
//奇数情况
for( j = 0 ; (i-j>=0)&&(i+j<array_len) ; j++ )//j为距i位置的距离
{
if( array[i-j] != array[i+j] )
break;
len_odd = 2*j+1;
max_len = (max_len>len_odd?max_len:len_odd);
}
//偶数情况
for( j = 0 ; (i-j>=0)&&(i+j+1)<array_len ; j++ )
{
if( array[i-j] != array[i+j+1] )
break;
len_even = 2*j+2;
max_len = (max_len>len_even?max_len:len_even);
}
}
return max_len==1?0:max_len;//单个数不为回文
}
中心扩展法代码测试及测试结果
中心扩展法测试代码与最长优先遍历法的测试代码格式相同,读者可更改不同的测试例来验证代码,若有出入欢迎留言指正。这里就不赘述测试代码和测试结果了。
方法4-Manacher算法
高能预警!! 本算法可能有点烧脑,读者请屏住呼吸,坐稳扶好(*^▽^*)
Manacher算法思想
Manacher,,笔者喜欢戏称它为“马拉车”,方便记忆~哈哈哈哈,下面结合书中的精华,对马拉车算法进行介绍。
Manacher算法首先通过在每个字符的两边都插入一个特殊的符号,将所有的奇数或偶数长度的回文子串都转换成奇数长度。
例如:“abba”的两边插入字符#变成“#a#b#b#a#”,“aba"的两边插入字符#变成”#a#b#a"。同时,为了进一步减少编码的复杂度和更好地处理越界问题,可以在字符串的开始加入另一个特殊字符,例如,在“#a#b#a#”的开始插入字符$,变成“$#a#b#a#”
参考自July著《编程之法》P20
以字符串“12212321”为例,插入#和$这两个特殊符号后,变成了S[]="$#1#2#2#1#2#3#2#1#",然后用一个数组P[i]来记录以字符S[i]为中心的最长回文子串向左或向右扩张的长度(注意:此长度包括S[i]!)。
这样,给定了S[i]之后,便能根据S[i]计算出P[i],如下:
S[i] | #1#2#2#1#2#3#2#1# |
---|---|
P[i] | 12125214121612121 |
可以看出,max(p[i]-1)正好是原字符串中最长回文子串的总长度
因此,本算法的关键在于如何求出P[i]数组!!
为了求出这个神奇的P[i]数组,Malacher算法增加了两个辅助变量:id 和 mx
id 表示最大回文子串中心位置
mx 表示最大回文子串边界,mx = id + P[id]
当 mx > i 时,则 P[i] >= min( P[2id-i] , mx-i )
当 mx < i 时,则令 P[i] = 1
对于上述两个表达式,本人理解如下:
1)当 mx > i 时,则 P[i] >= min( P[2id-i] , mx-i )
P[2id-i] 中 2id-i 是 i 关于 id 的对称点(可理解为 2id-i 在 id 的左边界位置, i 在 id 的右边界),则 mx-i 为向右扩张长度,P[2id-i] 为向左扩张长度。当 mx > i 时,说明 i 在最大回文边界内,但 i 自身的左边界或右边界可能会超出 ±mx ( -mx 表示 mx 关于 id 的对称点),当发生超出时,未超出部分在 mx 范围内一定对称,意味着 P[i] = P[j] ; 但是超出部分则不确定了,因此取了两者的最小值。
2)当 mx < i 时,则令 P[i] = 1
对于第二种情况,说明 i 一定不在最长子回文串范围内,由于无法对 P[i] 做更多的假设,因此赋值为1,在代码中会继续对它的左右位置元素再判定,从而准确更新 P[i] 。
Manacher算法复杂度
马拉车算法使用 id 和 mx 做配合,可实现在算法中直接对 P[i] 快速赋值,相比较中心扩展法而言,它减少了比较次数,最终保证了时间复杂度为***O(n)***。
Manacher算法代码实现及测试
最后贴出马拉车实现代码,笔者建议,为了更好地理解代码,可以用上面的例子结合代码走一遍。
你会发现,随着 mx 和 id 的更新,最大回文子串会越来越大,它的 mx 和 id 实际都是以编号的形式存在的。大于 mx 的部分一定会进 else 分支 。 就像上节本人理解2)所言,算法会对 else 分支的 i 的左右元素进行再匹配,更新P[i] 值。
若最大回文子串有“长大”的趋势,则算法会及时更新 mx(最大回文子串边界) ,并记录它的 id (最大回文中心位置),算法的最后,返回这个 id (实际就已经找到最大回文中心位置了),然后在P数组中找到 ***P[id]***,最终 P[id]-1 就是题目要求的最大回文子串长度。
下面贴出代码:
/************************
Author:tmw
date:2017-11-15
************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define min(a,b) (a<b?a:b)
#define max(x,y) (x>y?x:y)
/*原始串格式转换函数*/
char* Manacher_change(char* array)
{
int i;
char* temp;
temp = (char*)malloc( (strlen(array)*2+3)*sizeof(char) );//串尾加'$',串中间加'#',防越界
temp[0] = '$';//串首加'$'
for( i = 1 ; i < 2*strlen(array) ; i+=2 )
{
temp[i] = '#';
temp[i+1] = array[i/2];
}
temp[2*strlen(array)+1] = '#';
temp[2*strlen(array)+2] = '\0';//字符串结束标记
return temp;
}
int longest_sub_Palindrome_Manacher(char* s)
{
//字符串合法性检查
if(strlen(s)==0 || s==NULL)
return 0;
//将字符串改写成马拉车算法规定格式
char* S;
S = (char*)malloc( (strlen(s)*2+4)*sizeof(char) );
S = Manacher_change(s);
char* P;
P = (char*)malloc( (strlen(S)*sizeof(char)) );//为p数组申请与S同等空间
/*开始运行Manacher算法主体*/
int i,mx,id,ans;
mx = 0;//初始化最大回文子串边界长度
ans = 0;
for( i = 1 ; i < strlen(S) ; i++ )//给每一个P[i]赋值,S[]数组1号位存的是$,防越界的,不用考虑它
{
//当mx>i(目标中心在最大回文子串边界范围内),P[i]>=min(P[2id-i],mx-i)
if( mx > i )
P[i] = min( P[2*id-i], mx-i );
else//当mx<=i(目标中心在最大回文子串边界范围外),让P[i]=1,匹配待续
P[i] = 1;
while( S[i+P[i]] == S[i-P[i]] )//以i为中心,匹配更新P[i]
P[i]++;
if( i+P[i] > mx )//更新最大回文子串边界值,并记录最大回文子串中心位置id
{
mx = i+P[i];
id = i;
}
//max(P[I]-1)就是原字符串中最长回文子串的长度
ans = max(ans,P[i]);
}
return ans-1 == 1?0:ans-1;//单个元素不属于回文
}
Manacher算法测试代码与最长优先遍历法的测试代码格式相同,读者可更改不同的测试例来验证代码,若有出入欢迎留言指正。这里就不赘述测试代码和测试结果了。
梦想还是要有的,万一实现了呢~~ヾ(◍°∇°◍)ノ゙