5. 最长回文子串

51 阅读1分钟

题目

image.png

思路一 动态规划

dp[i][j]为true,字符串从i到j的子串为回文串,否则不是回文串。

当dp[i+1][j-1]为回文串时,且dp[i]与dp[j]相同,则dp[i][j]为回文串。

特殊情况:长度为0或1的字符皆为回文串,j-i<2。

复杂度

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

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

编码

/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function (s) {
  let res = '';
  let n = s.length;
  let dp = Array.from({ length: n }, () => new Array(n));
  // 这里i必须倒序遍历(后面要取dp[i+1])
  for (let i = n - 1; i >= 0; i--) {
    for (let j = i; j < n; j++) {
      // 主要看这里
      dp[i][j] = (s[i] === s[j]) && (j - i < 2 || dp[i + 1][j - 1]);
      // 找最长
      if (dp[i][j] && (j - i + 1 > res.length)) res = s.substring(i, j + 1);
    }
  }
  return res;
};

思路二 中心扩展算法

先预处理字符串,每个字符之间都加入‘#’,然后首尾添加^$,保证字符串长度为奇数。

中心扩展思想:分别以每个字符为中心向外扩展,尝试出该中心的最长回文串的长度,最终得到最长回文串的中心(maxIdx)以及长度(max),最长回文串的开始下标(start)为(maxIdx-max)/2,结束下标(end)为 start + max。

复杂度

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

空间复杂度:O(1)

编码

/**
 * @param {string} s
 * @return {string}
 */
function findLongestPalindrome(str) {
  // 拼接字符串(拼接后字符个数为奇数个)
  let tempStr = `^#${str.split('').join('#')}#$`;
  let max = 0, maxIdx = 0;

  for (let i = 1; i < tempStr.length - 1; i++) {
    let n = 0
    // 中心扩展,计算第i元素的最长回文串的半径
    while (tempStr[i - n - 1] === tempStr[i + n + 1]) {
      n += 1;
    }

    // 取最大值
    if (n > max) {
      max = n;
      maxIdx = i;
    }
  }

  // 计算最长回文串的起始位置
  // Math.floor((最长回文中心点-最长回文长度) / 2)
  let start = (maxIdx - max) >> 1;
  let end = start + max;
  return str.slice(start, end);
}

思路三 马拉车算法

马拉车算法为中心扩展算法的优化版本,利用已知条件判断以当前字符为中心的回文串最小长度。

情况一

比如 ABADABA,以D为中心,左右两边都为ABA,已知下标为i=1的最长回文串为ABA,半径为1;下标为j=3的最长回文串为ABADABA,半径为3。那么i以j为中心的对称点,下标为z=5的最长回文子串也为ABA,半径为1。

情况二

上面是最理想的情况,再比如:DABADABAE,在左边加了D,右边加了E。下标为i=2处DABAD是回文串,半径为2,下标为j=4处ABADABA也是回文串,半径为3,那么那么i以j为中心的对称点,下标为z=6处 + i处回文串半径2 = 8,超出了j=4处回文串的右边界7。这个情况只能保守处理,虽然不能保证边界外的字符依然是回文串,但能保证边界内的是回文串,所以我们取一个 z到右边界的距离 和 i处回文串半径的最小值,然后再继续中心扩展。

复杂度

时间复杂度:O(n)

空间复杂度:O(n)

编码

/**
 * @param {string} s
 * @return {string}
 */
function findLongestPalindrome(str) {
  // 拼接字符串(拼接后字符个数为奇数个)
  let tempStr = `^#${str.split('').join('#')}#$`;
  // 记录一个数组p,记录分别以每个字符为中心的最长回文串长度
  let p = new Array(tempStr.length).fill(0);
  let max = 0, maxIdx = 0;
  let center = 0, right = 0;

  for (let i = 1; i < tempStr.length - 1; i++) {
    // 马拉车优化1:设当前已知最长回文为a
    // 若i没超出a的右边界,那么i关于当前已知最长回文的中心点的对称点为i_mirror
    // 以i与i_mirror为中心的最长回文两者相关。
    // 设i_mirror为中心的最长回文半径为p[i_mirror],若i+p[i_mirror]>right,则超出的部分在对应i-p[i_mirror]上不一定相等。
    // 所以这里以右边界取了最小值
    if (i < right) {
      p[i] = Math.min(right - i, p[center - (i - center)]);
    }

    // 中心扩展,计算第i元素的最长回文串的半径
    while (tempStr[i - p[i] - 1] === tempStr[i + p[i] + 1]) {
      p[i] += 1;
    }

    // 马拉车优化2:更新右边界,以及对应的回文串中心点。
    if (right < i + p[i]) {
      right = i + p[i];
      center = i;
    }

    // 取最大值
    if (p[i] > max) {
      max = p[i];
      maxIdx = i;
    }
  }

  // 计算最长回文串的起始位置
  // Math.floor((最长回文中心点-最长回文长度) / 2)
  let start = (maxIdx - max) >> 1;
  let end = start + max;
  return str.slice(start, end);
}