最长回文子串(中心扩散、动态规划、Manacher)

295 阅读4分钟

给你一个字符串 s,找到 s 中最长的回文子串。

什么是最回文串??

回文串

“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。

最长回文串

示例 1:

输入:s = "babad"

输出:"bab"

解释:"aba" 同样是符合题意的答案。

示例 2:

输入:s = "cbbd"

输出:"bb"

解题方法一:中心扩散法

每一个位置出发,向两边扩散查找

1、向左侧查找, 左侧元素 = 当前元素时,继续向左侧查找,否则终止

2、向右侧查找, 右侧元素 = 当前元素时,继续向右侧查找,否则终止

3、向左右查找,左侧元素 = 右侧元素时,继续查找,否则终止

上述三种情况都需要判断向左时,不能超过字符串的最左位置即:left >= 0; 向右查找时不能超过字符串的最右位置即:right < s.length

image.png

/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function(s) { 
    if(!s){
        return "";
    }
    var slength = s.length,
        maxLength = 0,
        maxStart = 0;
    for(var i = 0; i < slength; i++) {
        // 从以i为中心,向两侧查找
        var left = i-1, right = i + 1; 
        var curlen = 1;
        // 左指针 向左移动,左侧元素与当前元素相同时curlen += 1
        while(left >= 0 && s.charAt(i) === s.charAt(left))  {
            left --;
            curlen ++;
        }
        // 右指针 向右移动 右侧元素与当前元素相同时curlen += 1
         while(right < slength && s.charAt(i) === s.charAt(right))  {
            right ++;
            curlen ++;
        }
        // 左指针往左,右指针往右。 当前元素的左侧和右侧相同时curlen += 2
        while(left >= 0 && right < slength && s.charAt(right) === s.charAt(left))  {
            left --;
            right ++;
            curlen += 2;
        }
        if(curlen > maxLength){
            maxLength = curlen;
            maxStart = left
        }

    }
    // maxStart 指向的是最长回文子串的左侧
    return s.substring(maxStart + 1, maxStart + maxLength + 1);
    
};

解题方法二:动态扩散

使用二维数组,记录子串从i到j是否为回文串,若i, j为回文串,则i-1到j-1必定为回文串

image.png

/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function (s) {
    // 生成二维矩阵  
    // 注:Array.prototype.fill() ,用一个固定值填充一个数组中从起始索引到终止索引内的全部元素,接收三个参数(value,start,end).
    const initialize2DArray = (w, h, val = null) => Array(h).fill().map(() => Array(w).fill(val))

    let len = s.length
    if (len < 2) {
        return s
    }
    // 记录最长子串 长度
    let maxLen = 1
    // 记录最长子串 开始位置
    let begin = 0
    // 生成一个矩阵  若从第i到第j位为回文串,则dp[i][j]赋值为true
    let dp = initialize2DArray(len, len, null)
    // 自己单独一个元素都是回文子串(矩阵的对角线)
    for (let i = 0; i < len; i++) {
        dp[i][i] = true
    }
    for (let j = 1; j < len; j++) {
        for (let i = 0; i < j; i++) {
            // s[i~j]  s[i] != s[j]说明第i位到第j位这段不是回文串
            if (s[i] != s[j]) {
                dp[i][j] = false
            } else {
                // s[i] == s[j]时,如果字符串长度小于或者等于3(j-i+1 <=3),s[i~j]必定是回文串
                if (j - i < 3) {
                    dp[i][j] = true
                } else {
                    //  s[i] == s[j]时,s[i+1,j-1]是其子串,若为回文串,则s[i,j]也是回文串;同理:子串若不是,则也不是
                    dp[i][j] = dp[i + 1][j - 1]
                }
            }
            // 如果s[i,j]是回文串,且当前回文串长度大于上一次的长度(即比较每次循环的回文串长度,取最大值)
            if (dp[i][j] && j - i + 1 > maxLen) {
                maxLen = j - i + 1
                begin = i 
            }
    }
  }
  // 返回从begin开始致begin + maxLen的子串
  return s.substring(begin, begin + maxLen)
}

解题方法三:Manacher算法

Manacher算法在中心扩散的法的基础上,存储了已经查找过的元素,然后根据回文串对称性,尽可能减少重复查找。

如下图: 回文串"ABCBDBCBA"关于D对称的两侧“C”位置(黑色箭头),两测以“C”为中心的回文串是相同的。因此当我们找到了左侧“C”为中心的回文串并且存储下来,右侧“C”为中心的回文串就不需要再查找了。这就是manacher算法核心思想,当然我们还需要对特殊情况进一步讨论。

image.png 步骤1:

为了将奇、偶数回文串统一,将原数组进行预处理,用一个字符(要求不在原字符串内)将原字符串隔开方便统一处理。

用数组p来记录每个元素为中心的回文半径(不包含自己

如图,以下标为9的元素为中心,查找的回文串是“#D#C#A#C#D#”,则回文半径是“#D#C#” 回文半径长度为 p[9] = 5;

image.png 不难发现,处理后的字符串回文半径p[i],就是原字符串的回文串的长度:

步骤二:

找到i关于center对称的位置mirro, 以mirror为中心的回文串半径为p[mirror];

情况1:p[mirror] + i < maxRight, 则说明p[i]的回文半径不会超出以center为中心的回文串半径,直接赋值p[i] = p[mirror]即可

情况2:p[7] + i >= maxRight, 则赋值p[i] = maxRight - i,然后使用中心法则,以left = i - (1 + p[i]); right = i + (1 + p[i])尝试向两侧查找;

3、如果i + p[i] > maxRight; 此时需要挪动maxRight到当前元素为中心的回文串右边界,即maxRight = i + p[i];将center指向当前循环到的元素,即i的位置

4、判断当前p[i]是否大于maxLen,若是则更新begin和maxLen

image.png

JavaScript实现:

/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function (s) {
    // 给字符添加分割符
    var str = addSeparator(s, '#');
    var strLen = str.length;
    var p = new Array(strLen);
    p.fill(0);
    var maxRight = 0;
    var center = 0;
    // 记录最长回文串的长度和起点
    var maxLen = 1;
    var begin = 0;
    for (var i = 1; i < strLen; i++) {
        if (i < maxRight) {
            // i 关于center对称的位置 center - (i - center)
            var morror = 2*center - i;
            // 判断p[morror] + i 是否在maxRight范围内, 
            // 情况1: p[morror] + i < maxRight; p[i] = p[morror];
            // 情况2: p[morror] + i >= maxRight; p[i] = maxRight - i; 然后中心法则继续向两侧查找
            // 因此取二者的最小值即可
            p[i] = Math.min(p[morror], maxRight - i);
        } 
        // 不在范围内,中心扩散,尝试向两侧查找,针对上述情况2
        var left = i - (1 + p[i]);
        var  right = i + (1 + p[i]);
        while (left >= 0 && right < strLen && str.charAt(left) === str.charAt(right)) {
            p[i] ++;
            left--;
            right++;
        }
        // 查找到当前元素的回文半径p[i],已经超过的maxRight时
        if (i + p[i] > maxRight) {
            // maxRight记录以当前元素为中心的回文串 最右侧位置
            maxRight = i + p[i];
            // center指向当前元素的位置
            center = i;
        }
        // 比较 当前回文半径 与 maxLen,判断是否需要更新maxLen和begin
        if (p[i] > maxLen) {
            maxLen = p[i];
            begin = (i - maxLen) / 2;
        }
    }
    return s.substring(begin, begin + maxLen)
}


/**
 * @param {string} s 
 * @param {string} separator
 * @return {string}
 */
var addSeparator = function(s, separator){
    if (s.indexOf(separator) > -1) {
        throw Error('参数错误:您传入的分隔符,在传入的字符串串中存在!');
    }
    var str = [separator];
    for(var i = 0; i < s.length; i++){
        str.push(s.charAt(i))
        str.push(separator)
    }
    return str.join('');

}