Longest Palindromic SubString(最长回文)

Longest Palindromic SubString(最长回文)

最长回文是指给定一个字符串,找出其中最长的回文。Longest Palindromic SubString介绍了几种算法,翻译记录一下。

方法1:最长公共子串

将字符串翻转,然后找出两个字符串的最长公共子串,这个子串同时也是最长回文。
这个方法看似正确,其实有缺陷。
如,S=”caba”,S’=”abac”,最长子串是aba,求解正确。
但是,对于S=”abacdfgdcaba”,S’=”abacdgfdcaba”,最长公共子串是abacd,显然,这个不是回文。
从中可以看出,错误原因是由于非回文的子串正好是原字符串的一部分。为了修正这个错误,需要在发现最长子串的时候,检查子串的下标是不是与翻转后的原始下标一致(原文是we check if the substring’s indices are the same as the reversed substring’s original indices,我的理解是对于最长公共子串,如果是合法的回文,相对于S的头部,S’的末尾的偏移位置应是一样的,如果不一样,则意味着该子串只是公共子串,并不具备回文属性),如果一致,则更新最长回文,否则跳过该回文。该算法使用动态规划算法,时间消耗 O(n2) O ( n 2 ) 。详见Longest Common SubString。该算法使用的是最长后缀算法,比我设想的动态规划要简洁许多,需要多复习。

方法2: 暴力破解

最简单最粗暴的算法,遍历所有可能,找到一个答案。
复杂性分析:

  • 时间复杂度: O(n3) O ( n 3 ) .假设n是输入字符串的长度,则共有n(n-1)/2种子串。检查是否回文需要花费O(n),总的复杂度就是 O(n3) O ( n 3 )
  • 空间复杂度: O(1) O ( 1 )

方法3: 动态规划

为改进暴力破解方案,我们需要首先观察如何避免回文检查过程中的不必要的重复计算。如字符串ababa,如果我们已经知道bab是个回文,显然ababa也是一个回文,因为左边和右边的字符是相同的。
我们定义P(i,j)如下:

P(i,j)={true,0,Si...Sj is palindromeotherelse P ( i , j ) = { t r u e , S i . . . S j   i s   p a l i n d r o m e 0 , o t h e r e l s e

因此:
P(i,j) = (P(i+1, j-1) and Si == Sj)
初始条件:
P(i,i) = true
P(i, i+1) = (Si == Si+1)

这是个很直接的DP算法,我们首先初始化一个和两个字符的回文,然后计算3个或者更多.

复杂度分析:

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

方法4: 中心扩展(Expand Around Center)

我们可以用常量空间, O(n2) O ( n 2 ) 的时间来解决这个问题。

我们观察到回文总是围绕某个中心的一个镜像。因此,回文可以从某个中心扩展,共有2n-1个中心。

为什么会有2n-1个中心?因为中心也可能会在两个字符中间。如abba,中心就在两个b中间。

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

private int expandAroundCenter(String s, int left, int right) {
    int L = left, R = right;
    while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
        L--;
        R++;
    }
    return R - L - 1;   
}

复杂度分析:

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

方法5: Manacher算法

Manacher算法复杂度是O(n),详见Manacher’s algorithm. (还没来的及研究,存档先)

附上我实现的几个方法:

版本1: 暴力破解

public String longestPalindrome(String s) {
    if (s == null || s.length() <= 1) {
        return s;
    }
    int size = 0;
    boolean isVliad = false;
    String sub = "";

    for (int i = 0; i < s.length(); i++) {
        for (int k = size; k < s.length() - i; k++) {
            isVliad = true;

            for (int x = i,y = i + k; x <= y; x++,y--) {
                if (s.charAt(x) == s.charAt(y)) {
                    System.out.println(String.format("%d-%d:%s", x, y, s.charAt(x)));
                } else {
                    isVliad = false;
                    break;
                }
            }

            if (isVliad && (k + 1) > size) {
                size = k + 1;
                sub = s.substring(i, i + k + 1);
                System.out.println(String.format("%d----%d:%s", i, k, sub));
            }
        }
    }
    System.out.println(sub);
    return sub;
}

毫无悬念,提交后直接超时。

版本2: 改进型的暴力破解

public String longestPalindrome(String s) {
    if (s == null || s.length() <= 1) {
        return s;
    }
    Map<Character, List<Integer>> charMap = new HashMap<>();
    for (int i = 0; i < s.length(); i++) {
        Character k = s.charAt(i);
        List<Integer> set = charMap.get(k);
        if (set == null) {
            set = new ArrayList<>();
            set.add(i);
            charMap.put(k, set);
        } else {
            set.add(i);
        }
    }
    boolean isValid = false;
    int size = 1;
    String sub = s.substring(0, 1);
    int tail = 0;

    for (int i = 0; i < s.length(); i++) {
        List<Integer> set = charMap.get(s.charAt(i));

        for (int k = set.size() - 1; set.get(k) > i; k--) {
            isValid = true;
            tail = set.get(k);

            if (size > (tail - i + 1)) {
                break;
            }

            for (int x = i,y = tail; x < y; x++,y--) {
                 if (s.charAt(x) == s.charAt(y)) {
    //                         System.out.println(String.format("%d-%d:%s", x, y, s.charAt(x)));
                 } else {
                     isValid = false;
                     break;
                 }
            }

            if (isValid) {
                 if (size < (tail - i + 1)) {
                     size = tail - i + 1;
                     sub = s.substring(i, tail + 1);
                     System.out.println(String.format("%d----%d:%s", i, k, sub));
                 }

                 break;
            }
        }
    }
    System.out.println(sub);
    return sub;
}

只是用了一些辅助手段,减少了遍历次数,提交通过,耗时144ms。(当然,我给不出复杂度分析。这是不是从一个侧面说明了工程方法可以解决算法问题?。。。)

版本3: 动态规划算法

public String longestPalindrome(String s) {
    if (s == null || s.length() <= 1) {
        return s;
    }

    boolean track[][] = new boolean[s.length()][s.length()];
    String longest = s.substring(0, 1);

    for (int i = 0; i < s.length(); i++) {
        track[i][i] = true;
    }

    int j = 0;
    for (int k = 1; k < s.length(); k++) {
        for (int i = 0; i < s.length() - k; i++) {
            j = i + k;
            if (k == 1 || track[i+1][j-1]) {
                if (s.charAt(i) == s.charAt(j)) {
                    track[i][j] = true;

                    if (j - i + 1 > longest.length()) {
                        longest = s.substring(i, j + 1);
                    }
                }
            }
        }
    }

    return longest;
}

不负众望,60ms。

版本4: 最长公共子串

public String longestPalindrome(String s) {
    if (s == null || s.length() <= 1) {
        return s;
    }

    StringBuilder sb = new StringBuilder();
    for (int i = s.length() - 1; i >= 0; i--) {
        sb.append(s.charAt(i));
    }

    String r = sb.toString();
    int track[][] = new int[s.length()][s.length()];
    String sub = s.substring(0, 1);
    String rsub = "";
    String tmp = "";

    for (int i = 0; i < s.length(); i++) {
        for (int j = 0; j < s.length(); j++) {
            if (s.charAt(i) == r.charAt(j)) {
                if (i == 0 || j == 0) {
                    track[i][j] = 1;
                } else {
                    track[i][j] = track[i-1][j-1] + 1;
                }

                if (track[i][j] > sub.length()) {
                    int start = i - track[i][j] + 1;
                    tmp = s.substring(start, i + 1);
                    rsub = r.substring(r.length() - i - 1, r.length() - start);

//                        System.out.println(String.format("%d-%d :: %d-%d", start, i + 1, r.length() - i - 1, r.length() - start- 1));
//                        System.out.println(tmp + "--" + rsub);
                    if (tmp.equals(rsub)) {
                        sub = tmp;
                        System.out.println(sub);
                    }

                }
            }
        }
    }

    return sub;
}

有点意外,同样是 O(n2) O ( n 2 ) 算法,耗时是366ms,比我的改进型暴力破解算法还要多。估计是字符串截取和比较导致多余的消耗。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值