最长回文子串(Longest Palindromic Substring)


title: 最长回文子串(Longest Palindromic Substring)
date: 2018-04-10 21:05:49
tags: Weekly Algorithm
categories: Weekly Algorithm
mathjax: true

Github地址
简书地址
CSDN地址

问题描述

给定一个字符串 s,找出其中最长的回文子串,假设给定字符串的长度最大维 1000.

例如:

输入: "babad"
输出: "bab"
注意: “aba” 也是正确的解,有多个解返回其中一个即可
输入:"cbbd"
输出:"bb"

回文串是指一个字符串对称,从最左边和最右边分别往最中间遍历,各个位置的字符都相同。解决这个问题,下面将从四个算法分别进行介绍。

1、暴力枚举法(不可取)

暴力枚举法是最简单、最容易想到的方法,其思路是:首先找到字符串 s 的所有子串,然后判断该子串是否是回文字符串,最后返回最长的回文子串。该方法虽然简单明了,但因其计算成本太高,该算法在实际中并不可取

由于一个长为 n 的字符串,共有 n ( n + 1 ) 2 \frac{n(n+1)}{2} 2n(n+1) 个连续子串,故寻找子串的时间复杂度为 O ( n 2 ) O(n^2) O(n2), 判断一个字符串是否维回文串的时间复杂度维 O ( n ) O(n) O(n),故

时间复杂度: O ( n 3 ) O(n^3) O(n3)

空间复杂度为: O ( 1 ) O(1) O(1)

下面是 Java 的实现代码:

public class LongestPalindromicSubstring {
    public String longestPalindrome(String s) {
        // 保存得到的最长回文子串的起始位置
        int left = 0, right = 0;
        int len = s.length();

        for (int i=0; i<len; ++i) {
            for (int j=i; j<len; ++j) {
                // 获取 s 的连续子串
                String subStr = s.substring(i, j+1);

                // 判断子串是否是回文字符串
                if (isPalindrome(subStr)) {
                    if (j-i > right-left) {
                        left = i;
                        right = j;
                    }
                }

            }
        }

        return s.substring(left, right+1);
    }

    private boolean isPalindrome(String s) {
        int len = s.length();
        int left = 0, right = len-1;

        while (left < right) {
            if (s.charAt(left) != s.charAt(right))
                return false;
            ++left;
            --right;
        }

        return true;
    }
}

2、中心扩展法(可取)

中心扩展法是根据暴力枚举法改进而来,主要是去除了一些不必要的子字符串的判断,**主要思路:**首先从字符串 s 中选择一个字符作为子字符串的中心字符,然后以该字符维中心依次往左右两边扩展,判断该子串的最左边和最右边的字符是否相同,相同则继续向两边扩展,不相同则停止扩展,该子串则是以该字符为中心的最长回文子串,这样就减少了很多在暴力枚举方法中不必要的字符的判断.

和暴力枚举方法比较,以"abacdfgdcaba"为例,假设我们以第一个字符 ‘c’ 为中心,中心扩展首先比较"acd",由于 ‘a’ 和 'd’不相同,则停止扩展,二暴力枚举还需比较 “bacdf” 和 “abacdfg”.该算法在扩展时需要同时考虑子串是奇数和偶数的情况.

由于需要依次迭代每个字符串中心,因此该迭代需要 O ( n ) O(n) O(n) 时间复杂度,同时从中心向两边扩展的复杂度维 O ( n ) O(n) O(n),因此:

时间复杂度: O ( n 2 ) O(n^2) O(n2)

空间复杂度为: O ( 1 ) O(1) O(1)

public class LongestPalindromicSubstring {

    public  static void main(String[] args) {
        System.out.println(new test().longestPalindrome("abacdfgdcaba"));
        System.out.println(new test().longestPalindrome("cbbd"));
        System.out.println(new test().longestPalindrome("babad"));
    }

    public String longestPalindrome(String s) {
        // 保存获得的最大回文子串
        String maxStr = "";
        int len = s.length();

        for(int i=0; i<len; ++i) {
            String subStr1 = isPalindrome(s, i, i);

            if (subStr1.length() > maxStr.length()) {
                maxStr = subStr1;
            }
            String subStr2 = isPalindrome(s, i, i+1);

            if (subStr2.length() > maxStr.length()) {
                maxStr = subStr2;
            }
        }

        return maxStr;
    }

    private String isPalindrome(String s, int i, int j) {
        // i 表示中心扩展的左边字符
        // j 表示中心扩展的右边字符
        int len = s.length();
        while (i >= 0 && j < len && s.charAt(i) == s.charAt(j)) {
            --i;
            ++j;
        }

        return  s.substring(i+1, j);
    }
}

3、动态规划(可取)

回文字符串的子串也是回文字符串,我们可以将最长回文子串分解为一些列子问题,使用动态规划.设 f 为状态表,f(i,j)表示字符区间 [i, j](包括j)是否为回文字符串
,f(i, j)=false 表示子串 [i, j] 不是回文字符串,f(i, j)=true 表示子串 [i, j] 为回文字符串.当我们判断了字符 [i], [j] 相同时,只需判断 f(i+1, j-1) 是否维 true 即可.

状态表满足以下关系:

$$

f(i,j)=\begin{cases}
true,\quad i=j \
s[i]==s[j], \quad i= j-1 \
s[i] = s[j]\quad and\quad f(i+1, j-1), \quad i< j-1
\end{cases}
$$

由于状态表 f 是一个 n*n 的方阵,且是一个对称方阵,故我们只需判断状态表 f 的右上角的内容,因此:

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

实现代码:

public class test {

    public  static void main(String[] args) {
        System.out.println(new test().longestPalindrome("abacdfgdcaba"));
        System.out.println(new test().longestPalindrome("cbbd"));
        System.out.println(new test().longestPalindrome("babad"));
    }

    public String longestPalindrome(String s) {
        int n = s.length();
        boolean[][] f = new boolean[n][n];

        int left =0, right=0;
        for (int j=0; j<n; ++j) {
            f[j][j] = true;
            for (int i=0; i<j; ++i) {
                if (s.charAt(i) == s.charAt(j) && (i == j-1 || f[i+1][j-1])) {
                    if (j-i > right - left) {
                        left = i;
                        right = j;
                    }

                    f[i][j] = true;
                }
            }
        }

        return s.substring(left, right+1);
    }
}

该代码首先将状态表全部初始化为 false, 然后按照从上到下,从左到右的顺序依次判断状态表的值.
4、Manacher 算法(马拉车算法)(可取)

Manacher 算法是一种经典的求取最长回文子串的方法,其基本原理是使用已知回文字符串的左半部分来推导以右半部分的字符为中心的回文字符.

我们使用 p[i] 表示以第 i 个字符为中心的最长回文半径.可以利用已知p[0],p[1]…p[i-1]的值,来计算 p[i] 的值.我们定义 maxRight 是当前计算 i 位置时所有回文子串所能达到的最右端的位置,且该回文串的中心位置为 k,此时有如下关系: maxRight = k + p[k],此时有两种情况:

pictures001

第一种情况:i > maxRight,此时初始化p[i] = 1, 然后判断s[i+p[i]] == s[i-p[i]],若不相等则停止,若相等,则++p[i]

if (i > maxRight) {
    p[i] = 1;
    while(s[i+p[i]] == s[i-p[i]]) {
        ++p[i];
    }
}

第二种情况:i <= maxRight,此时不在给 p[i] 赋值维为1,由回文串的对称性可得, 2k-i 是 i 关于 k 的对称点.此时由两种情况:

  1. 以 2k-i 为中心的回文串的半径(如图蓝色箭头)大于等于 m a x R i g h t − i maxRight - i maxRighti(空心箭头),由对称性可知,已知紫色箭头 5 和 6 关于 k 对称,且 2k-i 和 i 关于 k 对称,所以空心箭头 1 和 4 对称,2 和 3 对称,又箭头 7 和 8 对称,且箭头 1 和 2 分别是 7 和 8 的一部分,所以空心箭头 1 和 2 对称,故空心箭头 3 和 4 对称.所以p[i]的对称半径至少为 maxRight - i.所以首先 p[i]=maxRight - i,然后在依次往两边扩展判断是否对称.

picture2

  1. 以 2k-i 为中心的回文半径小于 maxRight-i,根据和上面类似的推导,可以得知 p[i] = p[2k-i],且不在扩展.

picture3

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n)
public class test {

    public  static void main(String[] args) {
        System.out.println(new test().longestPalindrome("abacdfgdcaba"));
        System.out.println(new test().longestPalindrome("cbbd"));
        System.out.println(new test().longestPalindrome("babad"));
    }

    public String longestPalindrome(String s) {

        StringBuilder temp = new StringBuilder("#");
        for (int i = 0; i < s.length(); ++i) {
            temp.append(s.charAt(i));
            temp.append("#");
        }
        String str = temp.toString();
        /*
        * maxCenter: 保存当前能延伸到最右端的回文字符串的中心位置
        * maxId: 保存当前最长回文子串的中心位置
        * p: 保存以该位置的字符维中心位置的最长回文字符的右边长度
        */
        int maxCenter=0, maxId=0;
        int n = str.length();
        int[] p = new int[n];
        for (int i=0; i<n; ++i) {
            int syncCenter = 2 * maxCenter - i;
            p[i] = (i<maxCenter + p[maxCenter) ? Math.min(p[syncCenter], maxCenter + p[maxCenter] - i) : 1;

            while(i-p[i] >=0 && i+p[i]<n && str.charAt(i-p[i]) == str.charAt(i+p[i])) {
                ++p[i];
            }

            if (i + p[i] >= p[maxCenter] + maxCenter) {
                maxCenter = i;
            }

            if (p[i] > p[maxId]) {
                maxId = i;
            }
        }
        return s.substring((maxId - p[maxId])/2, (maxId + p[maxId]) / 2);
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值