最长回文子串

问题描述

给定一个字符串,求它的最长回文子串的长度

方法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算法增加了两个辅助变量:idmx
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-ii 关于 id 的对称点(可理解为 2id-iid 的左边界位置, iid 的右边界),则 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算法复杂度

马拉车算法使用 idmx 做配合,可实现在算法中直接对 P[i] 快速赋值,相比较中心扩展法而言,它减少了比较次数,最终保证了时间复杂度为***O(n)***。

Manacher算法代码实现及测试

最后贴出马拉车实现代码,笔者建议,为了更好地理解代码,可以用上面的例子结合代码走一遍。
你会发现,随着 mxid 的更新,最大回文子串会越来越大,它的 mxid 实际都是以编号的形式存在的。大于 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算法测试代码与最长优先遍历法的测试代码格式相同,读者可更改不同的测试例来验证代码,若有出入欢迎留言指正。这里就不赘述测试代码和测试结果了。


梦想还是要有的,万一实现了呢~~ヾ(◍°∇°◍)ノ゙

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值