LeetCode笔记-最长回文子串【JavaScript】

304 阅读6分钟

Question:Longest Palindromic Substring

Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.

Example 1:

Input: "babad"
Output: "bab"
Note: "aba" is also a valid answer.

Example 2:

Input: "cbbd"
Output: "bb"

解法一:暴力枚举

/**
 * 算法实现
 * @param {string} s
 * @return {string}
 */
const longestPalindrome = function (s) {
  // 判断字符串是否回文
  function isPalindrome(str) {
    const len = str.length;
    const middle = parseInt(len / 2);
    for (let i = 0; i < middle; i++) {
      if (str[i] != str[len - i - 1]) {
        return false;
      }
    }
    return true;
  }
  // 获取任意起始位置,任意长度的字符串,判断其是否回文,保留长度最大的那个
  let res = '';
  let max = 0;
  const len = s.length;
  for (let i = 0; i < len; i++) {
    for (let r = i + 1; r <= len; r++) {
      const tmpStr = s.substring(i, r);
      if (isPalindrome(tmpStr) && tmpStr.length > max) {
        res = s.substring(i, r);
        max = tmpStr.length;
      }
    }
  }
  return res;
};

解法二:动态规划

动态规划(Dynamic Programming)是一种分阶段求解决策问题的数学思想。动态规划将一个问题拆成几个子问题,分别求解这些子问题,即可推断出大问题的解。

能采用动态规划求解的问题的一般要具有三个性质:

  • 1、最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
  • 2、无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
  • 3、有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势
/**
 * 算法实现
 * @param {string} s
 * @return {string}
 */
const longestPalindrome = function (s) {
  const len = s.length;
  // 生成二维数组,每个元素为false
  const dp = Array.from(new Array(len), () => new Array(len).fill(false));
  let res = '';
  // 第一层倒着循环,才能保证 dp[i+1][j-1] 已经存在
  for (let i = len - 1; i >= 0; i--) {
    for (let j = i; j < len; j++) {
      // 判断i 和 j下标的字符串相等时
      //如果间隔小于等于2,则代表length为 3以内的子字符串,则一定是回文子串
      //如果间隔 大于2时,则需要判断 dp[i+1][j-1] 是否为回文子串
      dp[i][j] = s.charAt(i) === s.charAt(j) && (j - i <= 2 || dp[i + 1][j - 1]);
      // 判断符合回文的最大子字符串
      if (dp[i][j] && j - i >= res.length) {
        res = s.slice(i, j + 1);
      }
    }
  }
  return res;
};

解法三:中心扩展法

回文串一定是对称的,每次选择一个中心,进行中心向两边扩展比较左右字符是否相等。

中心点的选取有两种:

  • aba,中心点是b,这时left:i,right:i
  • aa,中心点是两个a之间,这时left:i,right:i+1
/**
 * 算法实现
 * @param {string} s
 * @return {string}
 */
const longestPalindrome = function (s) {
  if (!s || s.length < 2) {
    return s;
  }
  let start = 0; let end = 0;
  const n = s.length;
  // 中心扩展法
  const centerExpend = (left, right) => {
    while (left >= 0 && right < n && s[left] == s[right]) {
      left--;
      right++;
    }
    return right - left - 1;
  };
  for (let i = 0; i < n; i++) {
    const len1 = centerExpend(i, i);
    const len2 = centerExpend(i, i + 1);
    // 两种组合取最大回文串的长度
    const maxLen = Math.max(len1, len2);
    if (maxLen > end - start) {
      // 更新最大回文串的首尾字符索引(除以2,并向下取整)
      start = i - ((maxLen - 1) >> 1);
      end = i + (maxLen >> 1);
    }
  }
  return s.substring(start, end + 1);
};

解法四:Manacher算法

Manacher算法,又叫“马拉车”算法,可以在时间复杂度为O(n)的情况下求解一个字符串的最长回文子串长度的问题。

1、字符串预处理

在进行Manacher算法时,首先会对字符串做一个预处理,在所有的空隙位置(包括首尾)插入同样的符号,要求这个符号是不会在原串中出现的。这样会使得所有的串都是奇数长度的。以插入#号为例:

aba  ———>  #a#b#a#
abba ———>  #a#b#b#a#

2、求回文半径

我们把一个回文串中最左或最右位置的字符与其对称轴的距离称为回文半径。Manacher定义了一个回文半径数组 RL,用 RL[i] 表示以第 i 个字符为对称轴的回文串的回文半径。

char:    # a # b # a #
 RL :    1 2 1 4 1 2 1
RL-1:    0 1 0 3 0 1 0
  i :    0 1 2 3 4 5 6

char:    # a # b # b # a #
 RL :    1 2 1 2 5 2 1 2 1
RL-1:    0 1 0 1 4 1 0 1 0
  i :    0 1 2 3 4 5 6 7 8

通过观察可以发现,RL[i]-1 的值,正是在原本那个没有插入过分隔符的串中,以位置i为对称轴的最长回文串的长度。那么只要我们求出了 RL 数组,就能得到最长回文子串的长度。

3、求RL数组

我们引入一个辅助变量 MaxRight,表示当前访问到的所有回文子串,所能触及的最右一个字符的位置。另外还要记录下 MaxRight 对应的回文串的对称轴所在的位置,记为 pos,它们的位置关系如下:

我们从左往右地访问字符串来求 RL,假设当前访问到的位置为 i,即要求 RL[i],在对应上图,i 必然是在 pos 右边。但我们更关注的是,i 是在 MaxRight 的左边还是右边。分情况讨论:

情况一:i 在 MaxRight 的左边

我们找到 i 关于 pos 的对称位置 j ,这个j对应的 RL[j] 我们是已经算过的。根据回文串的对称性,以 i 为对称轴的回文串和以 j 为对称轴的回文串,有一部分是相同的。

1、以 j 为对称轴的回文串比较短

2、以 j 为对称轴的回文串很长

不论以上哪种情况,之后都要尝试更新 MaxRightpos,因为有可能得到更大的 MaxRight

step 1: 令RL[i]=min(RL[2*pos-i], MaxRight-i)
step 2: 以 i 为中心扩展回文串,直到左右两边字符不同,或者到达边界
step 3: 更新 MaxRightpos

情况二:i 在 MaxRight 的右边

遇到这种情况,说明以 i 为对称轴的回文串还没有任何一个部分被访问过,于是只能从 i 的左右两边开始尝试扩展了,当左右两边字符不同,或者到达字符串边界时停止。然后更新 MaxRightpos

/**
 * 算法实现
 * @param {string} s
 * @return {string}
 */
const longestPalindrome = function (s) {
  if (!s || s.length < 2) {
    return s;
  }
  // 字符串预处理
  const s_f = `#${s.split('').join('#')}#`;
  let c = 0; let R = 0;
  const len = s.length;
  const t_len = s_f.length;
  let maxLen = 0;
  let maxIndex = 0;
  let originIndex = 0;
  const p = new Array(t_len);
  p[0] = 0;
  for (let i = 1; i < t_len - 1; i++) {
    const j = 2 * c - i;
    if (i < R) {
      p[i] = Math.min(p[j], R - i);
    } else {
      p[i] = 0;
    }
    let left = i - p[i] - 1;
    let right = i + p[i] + 1;
    // 尝试扩展,注意处理边界
    while (left >= 0 && right < t_len && s_f[left] == s_f[right]) {
      left--;
      right++;
      p[i]++;
    }
    if (i + p[i] > R) {
      c = i;
      R = i + p[i];
    }
    // 更新最长回文子串
    if (p[i] > maxLen) {
      maxLen = p[i];
      maxIndex = i;
      originIndex = parseInt((i - p[i]) / 2);
    }
  }
  return s.substring(originIndex, originIndex + maxLen);
};

参考资料