最长回文子串

回文串 就是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。回文子串,顾名思义,即字符串中满足回文性质的子串。比如输入字符串 "google”,由于该字符串里最长的对称子字符串是 "goog”,因此输出4。
解法一:
分别求以每个字符或每两个字符为中心的回文子串长度, 由于子字符串的长度可能是奇数也可能是偶数。长度是奇数的字符串是从 只有一个字符的中心 向两端延长出来,而长度为偶数的字符串是从一个 有两个字符的中心 向两端延长出来。因此程序中要把这两种情况都考虑进去。  所以要对每个字符都求这两种情况,并对两种情况进行比较,然后取大者。如:google,分别求:
字符g:奇数子串情况,即以g为中心,向两边扩散,可见长度为1,偶数子串情况,以g及g的下一个字符一起作为中心,且g和g的下一个字符首先要相等,可见长度为0.
字符o:奇为1,偶为4
字符o:奇为1,偶为0
。。。。
C++实现代码如下:
int getMaxSym2(char * str){ if(str == NULL) return0; int maxlength = 0; char *ptag = str; while(*ptag !='\0'){ //奇数子字符串
 char *left = ptag - 1; char *right = ptag + 1; int oddlenght = 1; while(left >= str && *right != '\0' && *left == *right){ left--; right++; oddlenght += 2; } if(oddlenght > maxlength){ maxlength = oddlenght; } //偶数子字符串
         left = ptag; right = ptag + 1; int evenlength = 0; while(left >= str && *right != '\0' && *left == *right){ left--; right++; evenlength += 2; } if(evenlength > maxlength){ maxlength = evenlength; } ptag++; } return maxlength; }
通过判断最后长度是否是奇数可以知道是否以一个字符为中心还是以两个字符为中心,从而可以找到子串的首尾字符。
时间复杂度为O(n^2)。
解法二: manacher算法
算法的基本思路是这样的:把原串每个字符中间用一个串中没出现过的字符分隔#开来(加了#之后,所有子串长度都只能是奇数了,想想为什么,因此,无需再考虑偶数的情况),同时为了防止越界,在字符串的首部也加入一个特殊符$,但是与分隔符不同。同时字符串的末尾也加入'\0'。
算法的核心:用辅助数组p记录以每个字符为核心的最长回文字符串 半径。也就是p[i]记录了以str[i]为中心的最长回文字符串半径。p[i]最小为1,此时回文字符串就是字符串本身。  
示例:原字符串 'abba’,处理后的新串 ' $#a#b#b#a #\0’,得到对应的辅助数组p=[ 0,1,2,1,2,5,2,1,2, 1]。  
C++实现程序如下,对应的变量解释在后面
//预处理,将str:abba转换为: $#a#b#b#a#\0(从1开始,也可以从0开始)
char * pre(char *str){
    int length = strlen(str);
    char *prestr = newchar[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算法的具体实现,包括:辅助数组的构建、最大字符串长度的获取。
//manacher算法
int getMaxSym3(char *str){
    char *prestr = pre(str);
    int mx =0, pi=1;//边界和对称中心
    int len = strlen(prestr);
    //辅助数组
    int *p = newint[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;
}

上面几个变量说明:pi记录具有遍历过程中最长半径的回文字符串中心字符串。mx记录了具有最长回文字符串的右边界。  

 

pi是最长回文字符串(淡蓝色)的中心,如果以j为中心的最大回文串如上如所示,那么i处的情况与j处相同(关于pi的两侧是对称的)。这样便减少了运算量,i的对称位置是 2*pi-i,上图可见p[i]=p[j]=p[2*pi-i]。

还有另外一种情况,就是j的一部分超出蓝色部分,这时p[i]=p[j]就不一定对了,如下图  

 

这就为什么有取最小值这个操作:

if(mx>i)
{    
    p[i]=min(mx-i,p[2*pi-i]);//核心
}

剩下的代码就很容易看懂了。

最后遍历一遍p数组,找出最大的p[i]-1就是所求的最长回文字符串长度,说明如下:
(1)因为p[i]记录插入分隔符之后的回文字符串半径,所以以i为中心的回文字符串长度(包括了#)为2*p[i]-1。例如:#b#b#,中间#的半径为3,故回文字符串长度(包括#)为2*3-1; 
(2)由于子串总是以#开头#结尾,所以(回文子串长度-1)/2便得到纯字符的回文子串长度。即(2*p[i]-1-1)/2

版权声明:本文为博主原创文章,未经博主允许不得转载。

你可能感兴趣的:(最长回文子串)