leetcode 5: 最长回文子串

pre

这条题目的常见时间复杂度为O(N^2),在看解答分析的时候,有一位博主给出了时间复杂度为O(N)的算法,这里我将着重讨论最后号称O(N)的算法。

题目

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。

示例 1:

输入: “babad”
输出: “bab”
注意: “aba”也是一个有效答案。
示例 2:

输入: “cbbd”
输出: “bb”

详细题目请直接访问原题[^题目]

分析

相关的思路在原题的解题报告中已经讨论到了[^解题报告],诸位可以直接阅读解题报告

方案1

首先考虑到回文的特性,那么第一个想法就是将字符串反转过来,找出两者的公共字符串

"eabac" 反转过来是 "cabae",最长公共字符串是"aba" 也就是答案了

但是这种方法存在问题,请考虑如下情况

"abac" 反转过来是"caba"

也就是在找到公共字符串的时候我们需要确认两个字符串是否在同一个位置。

到这里,我们可以计算算法的时间复杂度,首先扫描一遍的效率是O(n^2) ,针对每个位置需要确认最长公共字符串,然后判断是否对应同一个字符串,最后判断是否是最长,也就是实际的效率接近是O(2 * n^2)
空间复杂度为O(n^2),可以优化为O(2*n)

方案2

考虑回文的中心点,对于原来的字符串可能存在2n*1个中心点(考虑如下情况)

"ababa" 
"abba" 中心点在"bb"中间

针对每个中心点,依次扫描左右两边,从而确认最大值
时间复杂度为O( (2*n - 1) *n ), 空间复杂度为O(1)

方案3(或者可以被称为优化过之后的方案2)

思路来源于论坛大佬[^大佬思路]
我们这里基于方案2谈论方案三,我们可以通过方案2的思路计算前k个元素的最长回文字符串,这里我们可以缓存一个包含最远范围的回文字符串边界

初始字符串为 
a b a b a b c

扫描过前3a b a b a b c
0 1 2 ?
L   C   R

考虑上面这个案例,当我们扫描到第4个的时候 s[3] == ‘b’, 我们按照原来的思路,去考虑左右两边的情况,s[2] == s[4],,接下来扫描下一位s[1] ?= s[5], 但是我们发现当前扫描的索引(记为i),被包含在(C,R]这个范围内, 这样我们考虑回文的特性,坐标i的特征应该和 坐标为 C - (i - C)的有关系,如果C-(i-C) 位置在 [L,R] 之间存在回文字符串,那么坐标i也应该具有相应的位置(注意如果镜像坐标具有的回文字符串边界超出了缓存的边界,那么这边我们不能考虑超出的部分)

s[3]的镜像位置为s[1]
s[1]的回文范围为 s[0:2],该范围在[0:4],所以对应的s[2:4]也应该是回文的
所以s[3]初始检查的坐标应该为s[1] ?= s[5]

扫描过前5个时
a b a b a b c
0 1 2 2 1 ?
  L   C   R
s[5]的镜像位置为s[1],s[1]在缓存的回文范围内
s[1]的回文范围为s[0:2],超出缓存的范围,进行裁剪之后为s[1:1]
镜像位置为s[5:5]
所以s[5]初始检查的情况为s[4] ?= s[6]

基于这个优化,我们可以减少一部分不必要的扫描工作,但是至少直观上,这个算法并没有能够将原来的算法效率提升一个数量级,在博客的评论部分也讨论这个问题,这个算法在劣势情况下依旧是O(N^2), 理论上优于方案2,空间上复杂度为O(n),逊色于方案2

代码

方案1

class Solution {
   public String longestPalindrome(String s) {
        char[] cArray = s.toCharArray();

        int[][] lengthArray = new int[s.length()][s.length()];

        int start =-1;
        int len = -1;

        for(int i = 0 ; i < s.length(); i++){
            for(int j = 0 ; j < s.length();j++){
                if(cArray[i] == cArray[s.length()-j-1]){
                    int pre = 0;
                    if (i>0 &&j>0) {
                        pre = lengthArray[j-1][i-1];
                    }

                    lengthArray[j][i] = pre+1;
                    //检查是否构成回文
                    if((s.length() - j-1) == (i - lengthArray[j][i] +1)){
                        if(lengthArray[j][i] > len){
                            len = lengthArray[j][i];
                            start = s.length() - j-1;
                        }
                    }
                }
            }
        }
        String maxStr ="";
        if(len > 0)
            maxStr = s.substring(start,start+len);
        return maxStr;
    }
}

方案2

class Solution {
    public String expendString (String s ){
        StringBuffer T = new StringBuffer();
        for(int i= 0; i < s.length();i++){
            T .append("#");
            T.append(s.charAt(i));
        }
        T .append("#");
        return T.toString();
    }
    public int findMaxLength(String T ,int middleIndex ,int[] cacheArray ){
        if(cacheArray[middleIndex] > 0){
            return cacheArray[middleIndex];
        }
        int len = 1;
        while(middleIndex-len >= 0 && middleIndex+len < T.length()){
            if(T.charAt(middleIndex-len) == T.charAt(middleIndex+len)){
                //动态朝外扩张,减少重复计算
                len ++;
                continue;
            }else{
               break;
            }
        }
        cacheArray[middleIndex] = len -1;
        return cacheArray[middleIndex];
    }
    public String longestPalindrome(String s) {
        String T = expendString(s);
        int[] cacheArray = new int[T.length()];
        //进行一遍计算
        for(int i = 0 ; i < T.length() ; i++){
            findMaxLength(T,i,cacheArray);
        }

        int maxMiddleIndex = -1;
        int size = -1;
        for(int i = 0 ; i < T.length();i++){
            if(cacheArray[i] > size){
                size = cacheArray[i];
                maxMiddleIndex = i;
            }
        }
        StringBuffer maxStr = new StringBuffer();

        for(int i = maxMiddleIndex - size ; i < maxMiddleIndex +size ; i++){
            if(i%2 == 1){
                maxStr.append(T.charAt(i));
            }
        }
        return maxStr.toString();
    }
}

方案3

// Transform S into T.
// For example, S = "abba", T = "^#a#b#b#a#$".
// ^ and $ signs are sentinels appended to each end to avoid bounds checking
string preProcess(string s) {
  int n = s.length();
  if (n == 0) return "^$";
  string ret = "^";
  for (int i = 0; i < n; i++)
    ret += "#" + s.substr(i, 1);

  ret += "#$";
  return ret;
}

string longestPalindrome(string s) {
  string T = preProcess(s);
  int n = T.length();
  int *P = new int[n];
  int C = 0, R = 0;
  for (int i = 1; i < n-1; i++) {
    int i_mirror = 2*C-i; // equals to i' = C - (i-C)

    P[i] = (R > i) ? min(R-i, P[i_mirror]) : 0;

    // Attempt to expand palindrome centered at i
    while (T[i + 1 + P[i]] == T[i - 1 - P[i]])
      P[i]++;

    // If palindrome centered at i expand past R,
    // adjust center based on expanded palindrome.
    if (i + P[i] > R) {
      C = i;
      R = i + P[i];
    }
  }

  // Find the maximum element in P.
  int maxLen = 0;
  int centerIndex = 0;
  for (int i = 1; i < n-1; i++) {
    if (P[i] > maxLen) {
      maxLen = P[i];
      centerIndex = i;
    }
  }
  delete[] P;

  return s.substr((centerIndex - 1 - maxLen)/2, maxLen);
}

summary

这个优化思路是题目相关的,具有一定的参考价值,推广价值低。

[^题目] : https://leetcode-cn.com/problems/longest-palindromic-substring/description/

[^解题报告] : https://leetcode-cn.com/problems/longest-palindromic-substring/solution/

[^大佬思路] : https://articles.leetcode.com/longest-palindromic-substring-part-ii/

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