你须要的LeeCode题No.05——“最长回文子串”_一点课堂(多岸学院)

最长回文子串

题目:最长回文子串java

描述:给定一个字符串 s,找到 s 中最长的回文子串。你能够假设 s 的最大长度为 1000。算法

示例:编程

  • 输入: "babad"
  • 输出: "bab"
  • 注意: "aba" 也是一个有效答案。

解析

回文字符串可能咱们不少人在学习编程语言基础时都练习过,可是只是判断了一个字符串是不是回文的而已,而今天的题目要复杂得多了。若是咱们要找出全部的子串,再一一判断它是否是回文的,可能须要O(n<sup>3</sup>)的时间复杂度,因此咱们要对它进行优化。下面咱们要介绍的两种方法,能够将此问题的时间复杂度分别下降到O(n<sup>2</sup>)和O(n)。数组

方法一:中心扩展

回文字符串的特性就是中心对称,那么咱们能够假定每一个字符都是一个回文子串的中心字符,而后以此字符为中心向两侧扩展,直到再也不是回文子串为止。这样咱们就找到了以每一个字符为中心的最长回文子串,也就等于找到了最长的回文子串。不过这里有一点须要注意,那就是一个回文串的中心多是一个字符,例如"aba"就是以字符 'b' 为中心,也多是在两个字符之间,例如"abba"的中心就在两个 'b' 字符之间,只要处理好这两种状况,代码就很简单了,参考以下:app

public String longestPalindrome(String s) {
    int len = s.length();
    if(len==0)return "";

    int i = 0;
    int[] ends = new int[2];
    
    while(i<len){
        // 处理 "aba" 的状况
        getEnds(s, len, i-1, i+1, ends);
        // 处理 "abba" 的状况
        getEnds(s, len, i, i+1, ends);
        i++;
    }

    return s.substring(ends[0], ends[1]);
}

private void getEnds(String s, int len, int left, int right, int[] ends){
    while(left >= 0 && right < len && s.charAt(left)==s.charAt(right)){
        left--;
        right++;
    }
    if((ends[1]-ends[0])<(right-left-1)){
        ends[0] = left+1;
        ends[1] = right;
    }
}

方法二:Manacher算法

这个方法也称为“马拉车”算法,能够说是解决回文字符串最好的算法了,由于它的时间复杂度仅为O(n)。下面咱们一步步分析这个算法是如何作到如此高效的。编程语言

首先,咱们用 '#' 或某个字符串中不存在的字符将字符串的每一个字符分隔开,并在首尾也插入一个 '#',以下所示:函数

file

以上操做解决了由于长度的奇偶性带来的问题,新的字符串长度是 2*n+1,是始终为奇数的。学习

接下来和中心扩展同样,咱们也要肯定以一个字符为中心的最长回文子串的长度。Manacher算法中有一个概念叫做“回文半径”,它指的是从中心字符到最右侧(或最左侧)字符的距离,例如"#a#"的半径就是2,。用此距离构造一个半径回文数组radius,咱们根据数组中的最大值就能够肯定最长回文子串的长度了。例如,以上示例中间的字符 'b' 的回文半径为4,构成的回文数组以下:优化

file

能够发现,以原串第 i 个字符为中心的最长回文子串,就是radius数组第 i 位的值减 1 。那么,咱们如何构造这个数组呢?ui

假设咱们已经求得了位置 i 处(相对于Manacher构造的字符串)的回文半径,也就是radius[i]的值,并记它的最右侧位置为 mx,以下所示:

file

而后咱们要计算位置 j 处的回文半径,其中 j>i。因为radius[i]的值可能为0,也可能较大,因此 j 可能小于 mx,也可能大于 mx,咱们先来看 j<=mx 时的状况。以下所示,j<mx 时,j 关于 i 的对称点 i-(j-i)(咱们记为 k),也必定大于 mx关于 i 的对称点 i-(mx-i)(咱们记为mn),以下所示:

file

如今,让咱们把目光聚焦到 j 的对称点 k 上,它将为咱们计算 j 的回文半径提供线索。由于radius[k]的值可能很小,以 k 为中心的回文子串彻底被以 i 为中心的子串覆盖,也可能较大超出了 i 的范围,以下所示:

file file

对于第一种状况,能够确定 j 也将被 i 彻底覆盖,因此它的回文半径和 k 是相等的。第二种状况则代表从 mx 以后的部分是未知的,须要进一步计算。

如今咱们再来考虑 j>=mx 的状况,以下图所示,这时 j 的回文半径一点都没计算过,因此只能进一步进行计算。

file

根据以上思路,能够参考的代码以下:

public String longestPalindromeOptimize(String s) {
    if (s == null || s.length() == 0) {
        return "";
    }

    char[] charArr = manacherStr(s);
    // 构造回文半径数组
    int[] radius = new int[charArr.length];
    // 当前已经计算过的最右侧下标
    int mx = -1;
    // i 表示 mx 最大时,对应的回文子串的中心
    int i = -1;
    // 最长回文子串的长度
    int max = 0;
    // 最长回文子串的起点下标
    int maxIndex = -1;

    for (int j = 0; j < radius.length; j++) {
        // 2*i-j 是 j 相对于 i 的对称点,也就是文章中的 k。
        // 当 j<mx 时,因为以 k 为中心的子串可能被 i 彻底覆盖,也可能超出 i 的范围
        // 因此,计算 咱们只能计算出 radius[j] 在 mx-j这个范围内的值
        // 而当 j>mx 时,就须要彻底从头开始计算了
        radius[j] = j < mx ? Math.min(radius[2 * i - j], mx - j + 1) : 1;

        // 计算 j<mx 时超出 mx-j 范围内的部分,或者 j 本就大于 mx时
        while (j + radius[j] < charArr.length && j - radius[j] >= 0
                && charArr[j - radius[j]] == charArr[j + radius[j]]) {
            radius[j]++;
        }

        // 更新 mx 的值和 i 的值
        if (j+radius[j]>mx) {
            mx = j+radius[j]-1;
            i = j;
        }

        // 更新max的值
        if (max<radius[j]) {
            max = radius[j];
            // j 的起点坐标,相对于Manacher字符数组而言是 j-radius[j]+1
            // 而相对于原数组而言,这个值是由原数组的位置乘以2再加一获得的,因此直接除以2便可
            maxIndex = (j-radius[j]+1)/2;
        }
    }

    return s.substring(maxIndex,maxIndex+max-1);
}

private char[] manacherStr(String s){
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < s.length(); i++) {
        sb.append('#');
        sb.append(s.charAt(i));
    }
    sb.append('#');
    return sb.toString().toCharArray();
}

总结

Manacher算法若是咱们没有听过仍是很难想到的,不过不要气馁,咱们只是来学习的,想不到能学到也算赚到,可以理解它也算一笔不小的收获。下面这个题目较为简单,让咱们放松一下吧。

下题预告

题目:Z 字形变换

描述:将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。

好比输入字符串为 "LEETCODEISHIRING" 行数为 3 时,排列以下:

L   C   I   R
    E T O E S I I G
    E   D   H   N

以后,你的输出须要从左往右逐行读取,产生出一个新的字符串,好比:"LCIRETOESIIGEDHN"。

请你实现这个将字符串进行指定行数变换的函数:string convert(string s, int numRows);

示例 1:

  • 输入: s = "LEETCODEISHIRING", numRows = 3
  • 输出: "LCIRETOESIIGEDHN"

示例 2:

  • 输入: 输入: s = "LEETCODEISHIRING", numRows = 4
  • 输出: "LDREOEIIECIHNTSG"
  • 解释:
L     D     R
    E   O E   I I
    E C   I H   N
    T     S     G

相关源码请加QQ获取。


【感谢您能看完,若是可以帮到您,麻烦点个赞~】

更多经验技术欢迎前来共同窗习交流: 一点课堂-为梦想而奋斗的在线学习平台 http://www.yidiankt.com/

![关注公众号,回复“1”免费领取-【java核心知识点】] file

QQ讨论群:616683098

QQ:3184402434

想要深刻学习的同窗们能够加我QQ一块儿学习讨论~还有全套资源分享,经验探讨,等你哦! 在这里插入图片描述

相关文章
相关标签/搜索