最长回文子串

最长回文子串——Manacher 算法

1. 问题定义

最长回文字符串问题:给定一个字符串,求它的最长回文子串长度。
如果一个字符串正着读和反着读是一样的,那它就是回文串。

举个🌰 :

  • s="ababa", 最长回文长度为 5;即ababa
  • s="abccb", 最长回文长度为 4,即 bccb

2.暴力解法:

对于最长回文子串问题,最简单粗暴办法是:找到所有字符串的子串,遍历每一个子串以验证它们是否为回文串。一个子串由子串的起点和终点确定,因此对于一个长度为n的字符串,共有n^2个子串。这些子串的平均长度大约为n/2,因此这种解法的时间复杂度是O(n^3)

解法:

string findLongestPalindrome(string &s){

   if (s.empty()) {
       return "";
    }

    if (s.size() == 1) {
        return s;
     }
    // 字符串 长度
    unsigned long length = s.size();
    // 最长 回文 字符串 长度
    int maxLength = 0;
    // 最长 回文 字符串 起始地址
    int start = 0;

    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 "";
}

3. 动态规划

回文字符串的子串也是回文,所以对于母串s,我们用p[i][j] = 1(表示以i开始以j结束的子串)是回文字符串,那么p[i+1][j-1]也是回文字符串

  • 这样当s[i] = s[j]时,如果p[i+1][j-1]回文子串,则p[i][j]也是回文子串。

  • 如果p[i+1][j-1]不是回文子串或者s[i] != s[j],那么p[i][j]就不是回文子串。

  • 特别地,对于这样的字符串——只包含单个字符、或者两个字符重复,其均为回文串

    p[i][i] = 1;
    c[i][i+1] = 1, if(s[i] == s[i+1])
    
  • 这样需要额外的空间o(N^2),算法复杂度也是o(n^2).
    解法:

      static int const arrayLength = 100;
      string findLongestPalindrome(string &s){
          if (s.empty()) {
                return "";
          }
    
          if (s.size() == 1) {
               return s;
          }
          // 字符串 长度
          unsigned long length = s.size();
          // 最长 回文 字符串 长度
          int maxLength = 0;
          // 最长 回文 字符串 起始地址
          int start = 0;
          // 存储 所有 子字符串
          bool p[arrayLength][arrayLength] = {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;
              }
          }
    
          // 子串 长度(因为已经计算了单个或两个字重复的回文字符串,所以子串长度最低从3开始)
          // 计算 3 - length 所有子串中 所有最长子串
          for(int len = 3; len <= length; len++){
              // 子串 起始 地址
              // 在 字符串 中 找到 所有 长度为 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 "";
         }
    

这种解法,当最大的回文子串有多个时,取最后一个,如果要取第一个,则在 start = i后面加上break;即可。

C语言解法:

static int const arrayLength = 100;
/**
 找到 最长 回文 子串 (动态 规划 方法)
 
 @param string 字符串
 @param stringLength 字符串 长度
 */
void findLongestPalindromeTwo(char *string, int stringLength) {
    if (string == NULL || stringLength == 0) {
        return;
    }
    if (stringLength == 1) {
        printf("%s\n", string);
        return;
    }

    // 回文串 长度
    int maxLength = 0;
    // 起始 位置
    int startPosition = 0;
    // 辅助 数组(存储 所有 字符串)
    int helperArray [arrayLength][arrayLength] = {false};
    
    // 循环 遍历
    for (int tmpIndex = 0; tmpIndex < stringLength; tmpIndex++) {
        // 单个 字符 为 回文串
        helperArray[tmpIndex][tmpIndex] = true;
        if (string[tmpIndex] == string[tmpIndex + 1]) {
            // 相同 字符 比如 aa 也是 回文串
            helperArray[tmpIndex][tmpIndex + 1] = true;
            startPosition = tmpIndex;
            maxLength = 2;
        }
    }
    
    // 循环 遍历
    // 子串 长度(因为已经计算了单个或两个字重复的回文字符串,所以子串长度最低从3开始)
    // 计算 3 - length 所有子串中 所有最长子串
    for (int len = 3; len <= stringLength; len ++) {
        for (int i = 0; i <= stringLength - len; i ++) {
            int j = i + len - 1;
            if (helperArray[i + 1][j - 1] == true && string[i] == string[j]) {
                helperArray[i][j] = true;
                maxLength = len;
                startPosition = i;
            }
        }
    }
    
    if (maxLength > 1) {
        for (int tmpIndex = 0; tmpIndex < maxLength; tmpIndex++) {
            printf("%c", string[startPosition]);
            startPosition++;
        }
        printf("\n");
    }
}

4.中心扩展

  • 很明显所有的回文字符串都是对称的;

  • 长度为奇数回文字符串以最中间字符位置为对称轴左右对称。

  • 长度为偶数的回文串以中间两个字符的空隙为对称轴对称。

  • 因此,整个字符串中所有字符,以及字符间的空隙都有可能是某个回文子串的对称轴位置。可以遍历这些位置,在每个位置上同时向左向右扩展,直到左右两边字符不同或者到达边界。

  • 对于一个长度为n的字符串,这样的位置一共有n+n-1=2n-1个,在每个位置上平均要进行大约n/4次比较,此算法的时间复杂度为o(n^2).

解法:

    string findLongestPalindrome(string &s) {
    
        if (s.empty()) {
            return "";
        }
    
        if (s.size() == 1) {
            return s;
        }
    
        unsigned long length = s.size();
    
        int maxlength = 0;
    
        int start = 0;
    
        string tmpStr = s;
        for(int i = 0,k = 0; i <= length; i++){
            s.insert(k, "#");
            k = k + 2;
        }

        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++;
            }
        }
    
        if(maxlength > 0){
            int tmpMaxLength = (maxLength - 1)/2;
            int tmpStartPostion = start/2;
            return tmpStr.substr(tmpStartPostion,tmpMaxLength);
        }
        return "";
    }

C语言解法:

/**
 找到 最长 回文 子串 (中心 对称 方法)

 @param string 字符串
 @param stringLength 字符串 长度
 */
void findLongestPalindrome(char *string, int stringLength) {
    if (string == NULL || stringLength == 0) {
        return;
    }
    if (stringLength == 1) {
        printf("%s\n", string);
        return;
    }
    
    // 插入 特殊字符 后字符串 长度
    int tmpStringLength = stringLength * 2 + 1;
    // 开辟 新的字符串
    char *tmpString = malloc(sizeof(char) * tmpStringLength);
    // 新字符串 复制 旧字符串 并在空隙插入 '#'
    tmpString[0] = '#';
    for (int tmpIndex = 0; tmpIndex < stringLength; tmpIndex ++) {
        tmpString[tmpIndex * 2 + 1] = string[tmpIndex];
        tmpString[tmpIndex * 2 + 2] = '#';
    }
    
    // 回文串 长度
    int maxLength = 0;
    // 起始 位置
    int startPosition = 0;
    
    // 遍历 字符串
    for(int i = 0; i < tmpStringLength; i++) {
        int j = i - 1;
        int k = i + 1;
        while (j >= 0 && k < tmpStringLength && tmpString[j] == tmpString[k]) {
            if (k - j + 1 > maxLength ) {
                maxLength = k - j + 1;
                startPosition = j;
            }
            j --;
            k ++;
        }
    }
    
    if (maxLength > 1) {
        int tmpMaxLength = (maxLength - 1)/2;
        int tmpStartPostion = startPosition/2;
        
        for (int tmpIndex = 0; tmpIndex < tmpMaxLength; tmpIndex++) {
            printf("%c", string[tmpStartPostion]);
            tmpStartPostion++;
        }
        printf("\n");
    }
}

5. Manacher 算法

中心扩展的算法是存在缺陷的:

  • 由于回文字符串的奇偶性造成了不同性质的对称轴位置,因此要分两种情况进行处理。

  • 很多子串被重复多次访问,造成较差的时间效率。
    举个🌰 :

    s : a b a b a
    i : 0 1 2 3 4
    

i == 1i == 2时,左边的子串aba分别被遍历了一次。

A. 解决长度奇偶性带来的对称轴位置问题
Manacher算法首先对字符串做一个预处理,在所有的空隙位置(包括首尾)插入同样的符号,要求这个符号是不会出现在原串中出现的,这样会使得所有的串都是奇数长度的。已插入#号为例。

 aba  ———>  #a#b#a#
 abba ———>  #a#b#b#a#

插入的是同样的符号,且符号不存在于原串,因此子串的回文性不受影响,原来是回文的串,插完之后还是回文的,原来不是回文的,依然不是回文的。

B. 解决重复访问的问题
我们把一个回文中最左或最右位置的字符与其对称轴的距离称为回文半径。Manacher定义了一个回文半径数组RL,用RL[i]表示以第i个字符为对称轴的回文串的回文半径。我们一般对字符串从左往右处理,因此这里定义RL[i]为第i个字符为对称轴的回文串的最右一个字符与字符i距离。对于上面插入分隔符之后的两个串,可以得到RL数组。

   s:    # a # b # a #
 RL :    1 2 1 4 1 2 1
RL-1:    0 1 0 3 0 1 0
  i :    0 1 2 3 4 5 6

   s:     # a # b # b # a #
 RL :     1 2 1 2 5 2 1 2 1
RL-1:     0 1 0 1 4 1 0 1 0
  i :     0 1 2 3 4 5 6 7 8

上面我们还求了一下RL[i]-1。通过观察可以发现,RL[i]-1的值,正是在原来那个没有插入过分割符的串中,以位置i为对称轴的最长回文串的长度。那么只要我们求出RL数组,就能得到最长回文子串的长度。

那么问题就变成了,怎样高效地求RL数组,基本思路是利用回文串的对称性,扩展回文串。
我们再引入一个辅助变量MaxRight,表示当前访问到的所有回文子串,所能触及的最右一个字符的位置。另外还要记录下MaxRight对应的回文串的对称轴所在位置,记为pos,它们的位置关系如下。

image.png

我们从左往右地访问字符串来求RL,假设当前访问到的位置是i,即要求RL[i],在对应上图,i必然在pos右边,但是我们更关注的是,i是在MaxRight的左边还是右边,我们分情况分析。

  • ** 当i在MaxRight的左边**
    如下图所示:
image.png

我们知道,图中两个红色块之间(包括红色块)的串是回文的;并且以i对称轴的回文串,是与红色块间的回文串有所重叠的。我们找到i关于pos的对称位置j,这个j对应RL[i]我们已经算过的。根据回文串的对称性,以i为对称轴的回文串和以j为对称轴的回文串,有一部分是相同的。这里又有两种细分情况。

a. 以j为对称轴的回文串比较短,短到如下图所示:

image.png

这时我们知道RL[i]至少不会小于RL[j],并且已经知道了部分的以i为中心的回文串,于是我们可以令RL[i]=RL[j].但是以i对称轴的回文串可能实际上更长,因此我们试着以i为对称轴,继续向左右两边扩展,知道左右两边字符不同或者到达边界。
b.以j为对称轴的回文串很长,如下图所示:

image.png

这时,我们只能确定,两条蓝线之间的部分(及不超过MaxRight的部分)是回文的,于是从这个长度开始,尝试以i为中心向左右两边扩展,知道左右两边字符不同或者到达边界。

综上,我们只能获取RL[2*pos - i]MaxRight-1这两者中最小的值,来保证该范围内的字符串是回文字符串,RL[i] = min(RL[2*pos - i], MaxRight-1),之后都要尝试更新MaxRightpos,因为有可能得到更大MaxRight.

具体操作如下:

step 1: 令RL[i]=min(RL[2*pos-i], MaxRight-i)
step 2: 以i为中心扩展回文串,直到左右两边字符不同,或者到达边界。
step 3: 更新MaxRight和pos
  • iMaxRight的右边
    遇到这种情况,说明以i为对称轴的回文串还没有任何一部分被访问过,于是只能从i的左右两边开始尝试扩展了,当左右两边字符不同或者到达字符串边界时停止更新。然后更新MaxRight和pos。
    解法:

    string findLongestPalindrome3(string &s) {
    
          if (s.empty()) {
              return "";
          }
    
          if (s.size() == 1) {
              return s;
          }
    
          unsigned long length = s.size();
    
          int MaxRight = 0;
          int Maxlen = 0;
          int pos = 0;
          string tmpStr = s;
          for(int i = 0,k = 0; i <= length; i++){
              s.insert(k, "#");
              k = k + 2;
          }
          length = s.size();
    
          int *RL = new int[length]();
          memset(RL, 0x00, sizeof(length));
          for (int i = 0; i < length; i++) {
              if (i < MaxRight) {
                  RL[i] = min(RL[2*pos - i], MaxRight-1);
              }else {
                  RL[i] = 1;
              }
              while (i - RL[i] >= 0 && i+RL[i] < length && s[i - RL[i]] == s[i + RL[i]]) {
                  RL[i] += 1;
              }
              if (RL[i] + i - 1 > MaxRight) {
                  MaxRight = RL[i] + i - 1;
                  pos = i;
              }
              Maxlen = max(Maxlen, RL[i]);
          }
          if (Maxlen > 0) {
              return tmpStr.substr((pos+1)/2 - Maxlen/2, Maxlen - 1);
          }
          free(RL);
          return "";
      }
    

C语言解法:

/**
 找到 最长 回文 子串 (拉马车 方法)
 
 @param string 字符串
 @param stringLength 字符串 长度
 */
void findLongestPalindromeThree(char *string, int stringLength) {
    if (string == NULL || stringLength == 0) {
        return;
    }
    if (stringLength == 1) {
        printf("%s\n", string);
        return;
    }

    // 插入 特殊字符 后字符串 长度
    int tmpStringLength = stringLength * 2 + 1;
    // 开辟 新的字符串
    char *tmpString = malloc(sizeof(char) * tmpStringLength);
    // 新字符串 复制 旧字符串 并在空隙插入 '#'
    tmpString[0] = '#';
    for (int tmpIndex = 0; tmpIndex < stringLength; tmpIndex ++) {
        tmpString[tmpIndex * 2 + 1] = string[tmpIndex];
        tmpString[tmpIndex * 2 + 2] = '#';
    }
    
    
    // 记录 最长 半径 范围
    int maxRight = 0;
    // 记录 最长 回文 字符串 长度
    int maxLength = 0;
    // 当前 最长 半径 的 对称轴
    int currentPosition = 0;
    // 记录 每个 位置 最长回文 长度
    int *palindromeArray = malloc(sizeof(int) * tmpStringLength);
    memset(palindromeArray, 0x00, sizeof(tmpStringLength));
    
    // 遍历 字符串
    for (int tmpIndex = 0; tmpIndex < tmpStringLength; tmpIndex++) {
        // 当前 字符 在最大 半径 范围 左边
        if (tmpIndex < maxRight) {
            if (palindromeArray[2*currentPosition - tmpIndex] > maxRight - tmpIndex) {
                palindromeArray[tmpIndex] = maxRight - tmpIndex;
            }
            else {
                palindromeArray[tmpIndex] = palindromeArray[2*currentPosition - tmpIndex];
            }
        }
        // 当前 字符 在 最大半径 范围 右边(没有被遍历过)
        else {
            palindromeArray[tmpIndex] = 1;
        }
        
        // 在先前 计算的 回文长度 基础 上 扩展遍历
        while (tmpIndex - palindromeArray[tmpIndex] >= 0 &&
               tmpIndex + palindromeArray[tmpIndex] < tmpStringLength &&
               tmpString[tmpIndex - palindromeArray[tmpIndex]] == tmpString[tmpIndex + palindromeArray[tmpIndex]]) {
            palindromeArray[tmpIndex] += 1;
        }
        
        if (palindromeArray[tmpIndex] + tmpIndex - 1 > maxRight) {
            maxRight = palindromeArray[tmpIndex] + tmpIndex - 1;
            currentPosition = tmpIndex;
        }
        
        // 更新 长度
        if (maxLength < palindromeArray[tmpIndex]) {
            maxLength = palindromeArray[tmpIndex];
        }
    }
    
    if (maxLength) {
        int tmpMaxLength = maxLength - 1;
        int tmpStartPostion = (currentPosition + 1)/2 - maxLength/2;
        
        for (int tmpIndex = 0; tmpIndex < tmpMaxLength; tmpIndex++) {
            printf("%c", string[tmpStartPostion]);
            tmpStartPostion++;
        }
        printf("\n");
    }
    
}

C.复杂度分析:

  • 空间复杂度:插入分隔符行程新串,占用了线性的空间大小;RL数组也占用线性 大小的空间,因此空间复杂度是线性的。

  • 时间复杂度:尽管代码里面有两层循环,由于内层循环只是对尚未匹配的部分进行,因此对于每一个字符而言只会进行一次,因此时间复杂度是o(n).

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 问题定义 最长回文子串问题:给定一个字符串,求它的最长回文子串长度。 解法1:暴力解法 找到字符串的所有子串,判断...
    HITMiner阅读 658评论 0 2
  • 这次要记录的是一个经典的字符串的题目,也是一个经典的马拉车算法的实践。相信在很多地方都会考到或者问到这道题目,这道...
    柠檬乌冬面阅读 2,900评论 0 9
  • 最长回文串问题是一个经典的算法题。 0. 问题定义 最长回文子串问题:给定一个字符串,求它的最长回文子串长度。如果...
    曾会玩阅读 3,999评论 2 25
  • 问题:给定一个字符串,求它的最长回文子串长度。提示:如果一个字符串正着读和反着读是一样的,那它就是回文串。下面是一...
    KevinHwong阅读 495评论 0 0
  • 上一篇KMP算法之后好几天都没有更新,今天介绍最长回文子串。 首先介绍一下什么叫回文串,就是正着读和倒着读的字符顺...
    zero_sr阅读 2,254评论 2 8