最长回文子串

原文链接:https://www.leahy.club/archives/%E7%AE%97%E6%B3%95%E4%B8%8E%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2

1. 题目

2. 解答

常见的解法主要有:

暴力法:列举出所有可能的子串,并对每个子串进行分析。

最长公共子串法:根据回文的定义,正反读是一样的。那么可以将原理来的字符串倒置,从而将问题转化成了求解最长公共子串的问题。求解最长公共子串可以使用动态规划。

动态规划的状态转移方程:

a r r [ i ] [ j ] = a r r [ i 1 ] [ j 1 ] + 1 arr[i][j]=arr[i-1][j-1]+1 arr[i][j]=arr[i1][j1]+1

a r r [ i ] [ j ] arr[i][j] arr[i][j]保存的是公共子串的长度。

暴力法的优化:暴力法中存在很多重复判断,这里采用空间换时间的做法,存储中间结果换取低时间复杂度。

首先定义 p ( i , j ) p(i,j) p(i,j),

p ( i , j ) = { <mstyle displaystyle="false" scriptlevel="0"> t r u e </mstyle> <mstyle displaystyle="false" scriptlevel="0"> <mtext> s[i,j]是回文串 </mtext> </mstyle> <mstyle displaystyle="false" scriptlevel="0"> f a l s e </mstyle> <mstyle displaystyle="false" scriptlevel="0"> <mtext> s[i,j]不是回文串 </mtext> </mstyle> p(i,j)= \begin{cases} true& \text{s[i,j]是回文串}\\ false& \text{s[i,j]不是回文串} \end{cases} p(i,j)={truefalses[i,j]是回文串s[i,j]不是回文串

接下来有: p ( i , j ) = p ( i + 1 , j 1 ) & & ( s [ i ] = = s [ j ] ) p(i,j)=p(i+1,j-1) \&\& (s[i]==s[j]) p(i,j)=p(i+1,j1)&&(s[i]==s[j])

中心扩展算法:

回文串一定是对称的,所以我们可以每次循环选择一个中心,进行左右扩展,判断左右字符是否相等即可。

Manacher算法(马拉车)算法:

马拉车算法 Manacher‘s Algorithm 是用来查找一个字符串的最长回文子串的线性方法,由一个叫 Manacher 的人在 1975 年发明的,这个方法的最大贡献是在于将时间复杂度提升到了线性。

详见:马拉车算法

3. 代码

3.1 暴力法

时间复杂度: O ( n 3 ) O(n^{3}) O(n3);空间复杂度: O ( 1 ) O(1) O(1)

//1.暴力法
    public static String solution(String s) {

        if (s == null)
            return null;

        int len = s.length();
        String res = s.substring(0,1);

        for(int i = 0; i < len; i++) {
            for (int j = i+1; j <= len; j++) {
                String subStr = s.substring(i,j);
                if (isPalindrome(subStr))
                    res = (subStr.length() > res.length()) ? subStr : res;
            }
        }
        return res;
    }

private static boolean isPalindrome(String s) {
        int n = s.length();

        for (int i = 0; i < n / 2; i++) {
            if (s.charAt(i) != s.charAt(n - i - 1))
                return false;
        }
        return true;
    }
3.2 最长相同子串法

时间复杂度: O ( n 2 ) O(n^{2}) O(n2);空间复杂度: O ( n ) O(n) O(n).

//2.最长相同子串法
    public static String solution2(String s) {

        if (s == null)
            return null;

        int len = s.length();
        String origin = s;
        String reverse = new StringBuilder(s).reverse().toString();
        int[][] arr = new int[len][len];
        int maxLen = 0;
        int maxEnd = 0;

        for (int i = 0; i < len; i++) {
            for (int j = 0; j < len; j++) {
                if (origin.charAt(i) == reverse.charAt(j)) {
                    if (i == 0 || j == 0)
                        arr[i][j] = 1;
                    else
                        arr[i][j] = arr[i-1][j-1] + 1;
                }

                if (arr[i][j] > maxLen) {
                    int beforeRev = len - j - 1;
                    if (beforeRev + arr[i][j] - 1 == i) {
                        maxLen = arr[i][j];
                        maxEnd = i;
                    }
                }
            }
        }
        return s.substring(maxEnd - maxLen + 1, maxEnd + 1);
    }


    //3.最长子串优化版
    public static String solution3(String s) {

        if (s == null || s.length() == 0)
            return null;

        int len = s.length();
        String origin = s;
        String reverse = new StringBuilder(s).reverse().toString();
        int[] arr = new int[len];
        int maxLen = 0;
        int maxEnd = 0;

        for (int i = 0; i < len; i++) {
            for (int j = len - 1; j >= 0; j--) {
                if (origin.charAt(i) == reverse.charAt(j)) {
                    if (i == 0 || j == 0)
                        arr[j] = 1;
                    else
                        arr[j] = arr[j-1] + 1;
                }
                else
                    arr[j] = 0; //之前使用二维数组,不需要置零,现在需要。

                if (arr[j] > maxLen) {
                    int beforeRev = len - j - 1;
                    if (beforeRev + arr[j] - 1 == i) {
                        maxLen = arr[j];
                        maxEnd = i;
                    }
                }
            }
        }
        return s.substring(maxEnd - maxLen + 1, maxEnd + 1);
    }
3.3 暴力法的优化(采用动态规划)

时间复杂度: O ( n 2 ) O(n^{2}) O(n2);空间复杂度: O ( n ) O(n) O(n)

//4.暴力法的优化(采用动态规划)
    public static String solution4(String s) {
        if (s == null || s.length() == 0)
            return s;

        int len = s.length();
        String res = s.substring(0,1);
        boolean[][] p = new boolean[len][len];

        for (int i = 1; i <= len; i++) { //i表示考察的字符串的长度
            for (int start = 0; start < len; start++) {
                int end = start + i - 1;
                //超出索引范围
                if (end >= len)
                    break;
                //单独判断长度为1和2的情况
                if (i == 1 || (i == 2 && s.charAt(start) == s.charAt(end)))
                    p[start][end] = true;

                else
                    p[start][end] = (s.charAt(start) == s.charAt(end)) && p[start+1][end-1];

                if (p[start][end] == true)
                    res = (end - start + 1 > res.length()) ? s.substring(start, end + 1) : res;
            }
        }
        return res;
    }

    //5.暴力解法的优化(动态规划写法二)
    public static String solution5(String s) {
        if (s == null || s.length() == 0)
            return s;

        int len = s.length();
        String res = "";
        boolean[][] p = new boolean[len][len];

        for (int i = len - 1; i >= 0; i--) {
            for (int j = i; j < len; j++) {
                p[i][j] = (s.charAt(i) == s.charAt(j)) && (j - i < 2 || p[i+1][j-1]); //j-i < 2表示长度为1和2的情况
                if (p[i][j] == true)
                    res = (j - i + 1 > res.length()) ? s.substring(i, j + 1) : res;
            }
        }
        return res;
    }

    //6.暴力解法的优化(动态规划写法二)的优化
    public static String solution6(String s) {
        if (s == null || s.length() == 0)
            return s;

        int len = s.length();
        String res = "";
        boolean[] p = new boolean[len];

        for (int i = len - 1; i >= 0; i--) {
            for (int j = len - 1; j >= i; j--) {
                p[j] = (s.charAt(i) == s.charAt(j)) && (j - i < 2 || p[j-1]); //j-i < 2表示长度为1和2的情况
                if (p[j] == true)
                    res = (j - i + 1 > res.length()) ? s.substring(i, j + 1) : res;
            }
        }
        return res;
    }
3.4 中心扩展算法

时间复杂度: O ( n 2 ) O(n^{2}) O(n2);空间复杂度: O ( 1 ) O(1) O(1)

//7.中心扩展算法
    public static String solution7(String s) {
        if (s == null || s.length() <= 1)
            return s;
        int len = s.length();
        int start = 0;
        int end = 0;

        for (int i = 0; i < len; i++) {
            int len1 = expandAroundCenter(s, i,i);
            int len2 = expandAroundCenter(s, i, i+1);
            int maxLen = Math.max(len1, len2);
            if (end - start < maxLen) {
                start = i - (maxLen-1)/2;
                end = i + maxLen/2;
            }
        }
        return s.substring(start, end+1);
    }

    private static int expandAroundCenter(String s, int left, int right) {
        int p1 = left, p2 = right;
        while (p1 >= 0 && p2 < s.length() && s.charAt(p1) == s.charAt(p2)) {
            p1--;
            p2++;
        }
        return p2 - p1 - 1;
    }
3.5 Manacher算法

时间复杂度: O ( n ) O(n) O(n);空间复杂度: O ( n ) O(n) O(n)

//8.马拉车算法
    public static String solution8(String s) {
        if (s == null || s.length() <= 1)
            return s;

        //第一步预处理
        String t = "$";
        for (int i = 0; i < s.length(); i++)
            t += "#" + s.charAt(i);
        t += "#@";

        //第二步计算数组p、起始索引、最长回文半径
        int n = t.length();
        int[] p = new int[n];
        int id = 0, mx = 0; //id能够延伸到最右端的那个回文子串的中心点位置;mx表示id对于的最右端位置
        //最长回文子串长度
        int maxLen = -1;
        //最长回文子串的中心位置索引
        int index = 0;
        for (int j = 1; j < n - 1; j++) {

            //最关键的逻辑:当回文串能够延伸到的最右端位置mx大于j时,
            //取与j对称的点p[2*id-j]的回文串半径与mx-j二者中较小的
            //作为p[j]的临时值,之后向左右两边进行扩展,如果扩展成
            //功则需要更新mx和id的值
            p[j] = mx > j ? Math.min(p[2*id-j], mx-j) : 1;

            //向左右两边扩展,扩展右边界
            while (t.charAt(j+p[j]) == t.charAt(j-p[j]))
                p[j]++;
            //如果回文子串的右边界超过了mx,则需要更新mx和id的值
            if (mx < p[j] + j) {
                mx = p[j] + j;
                id = j;
            }
            //如果回文子串的长度大于maxLength,则更新maxLen和index的值
            if (maxLen < p[j] - 1) {
                maxLen = p[j] - 1;
                index = j;
            }
        }

        //第三步截取字符串,输出结果
        int start = (index-maxLen)/2;
        return s.substring(start, start + maxLen);
    }