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

一、暴力for循环 (时间复杂度O(n^3)

由于太过“暴力”,就暂不多介绍。

二、中心扩散法 (时间复杂度O(n^2)

中心思想:

        1. 选取一个元素或两个(必须相同)元素作为回文串的对称中心

        2. 然后再往两边同时扩散(往两边遍历)。

        3. 直到遇到左边元素(i--)不等于右边元素(i++),或者任意一边没有元素了,就停止扩散

        4. 比较当前遍历的回文字符串与之前最大长度回文字符串长度,若大于则更新最长回文字符串。

注意:由于回文串长度可能是奇数长度,也可能是偶数长度,其选取对称中心也不相同

1. 偶数长度回文字符串:

                

                           图 1                                                                      图 2

function longestPalindrome(str) {
  const strArr = str.split('');
  let middleArr = []; // 用来装扩散时的回文子串,这里我叫其 *暂存数组*
  let maxStr = strArr[0]; // 记录最长回文子串,初始默认为一个长度的回文串。
  // 用于向左右扩散的左右指针
  let left = 0, right = 0;

  // 寻找最大奇数长度回文字符串
  for (let i = 0; i < strArr.length; i++) {
    // 左右设置两个指针的初始值,此时中心对称点即为下标i。
    left = i - 1;
    right = i + 1;
    middleArr = [strArr[i]];
    // 循环条件:当中心点左边元素等于右边元素,并且左右元素均存在。
    while (strArr[left] === strArr[right] && strArr[left] && strArr[right]) {
      middleArr.unshift(strArr[left]); // 如果左右元素都符合条件,则左边元素装到暂存数组头部
      middleArr.push(strArr[right]);   // 右边元素装到暂存数组尾部
      left--; // 左指针左移,右指针右移
      right++; 
    }
    // 当前轮扩散结束后,与之前最大回文字符串作比较。
    if (middleArr.length > maxStr.length) {
      maxStr = middleArr.join('');
    }
  }

  // 寻找最大偶数长度回文字符串 (基本原理同上)
  for (let i = 0; i < strArr.length - 1; i++) {
    left = i; // 注意: 因为是偶数长度回文字符串,所以其对称中心为两个相同的元素。
    right = i + 1;
    middleArr = [];
    while (strArr[left] === strArr[right] && strArr[left] && strArr[right]) {
      middleArr.unshift(strArr[left]);
      middleArr.push(strArr[right]);
      left--;
      right++;
    }
    if (middleArr.length > maxStr.length) {
      maxStr = middleArr.join('');
    }
  }
  return maxStr;
}

着重于思路,代码可优化。

三、动态规划 (时间复杂度O(n^2)

1.状态转移方程:dp[ i ][ j ] = ( s[ i ] == s[ j ] )  and ( j - i < 3 or dp[ i + 1][ j - 1] )

       估计之前没有接触过动态规划的同志(me too), 有点懵逼。在理解这个状态转移方程之前,先概述一下动态规划的思想

         当前状态需要依赖之前的状态(比如这里,当前字符串是否为回文,需要依赖他内部的字符串是否为回文),以空间换时间。

 到这里还是很懵逼,稳住,文章后面还有~~~

2. 用动态规划求回文字符串的思路 (用例:'babab')

                            图 3                                                                          图4

i : 当前字符串的子串的开始下标。

j : 当前字符串的子串的结束下标。

s: 当前字符串。

dp[ i ][ j ]: 以i为开始下标,j为结束下标的字符串是否为回文字符串(状态)。

图3解释:

  • 我们用dp[ i ][ j ](二维数组,先不要想其空间结构)来表示下标从 i => j 的字符串是否为回文字符串,是,则其值为TRUE, 反之, 为FALSE。
  • 相当于在填一个二维表,由于回文字符串是对称的,所以我们只需要考虑 i <= j 的情况,即 i 始终为回文字符串头下标j 始终为回文字符串尾下标
  • 如图3中, dp[ 0 ][ 1 ]表示下标 0 到 1 的字符串,即 ‘ba’,这当然不是一个回文字符串,所以为FALSE。

状态依赖(dp[ i + 1][ j - 1]):

  • 我们可以想象一下字符串 ‘babab’ 是否为回文字符串,是不是要求其中间 ‘aba’ 必须为回文字符串,‘bab’ 是否为回文字符串,又要求 ‘a’ 为回文字符串。所以前面dp[ i + 1][ j - 1]表示的就是当前字符串是否为回文,需要依赖它中间的前一个字符串的状态dp[ i + 1][ j - 1],若为TRUE,则dp[ i ][ j ]也为TRUE,反之为FALSE,当然前提是s[ i ] == s[ j ]。

提示需要仔细研究一下图3。

上面介绍了一些状态方程的核心概念,下面结合代码来分析:

function reverseStr(str) {
  const strArr = str.split('');
  // 用来装各个字符串的回文状态(true或者false, 后面会将其变成一个二维,如status[i][j]),
  let statusArr = []; 
  let middleArr = [];
  // 记录最长回文字符串长度,初始为1
  let maxLen = 1;
  // 记录最长回文字符串开始下标
  let startIndex = 0; 
  // 初始化对角线(i = j, 即长度为1的字符串,当然为回文字符串,所以填true)
  for (let i = 0; i < strArr.length; i++) {
    middleArr[i] = true;
    statusArr[i] = middleArr;
    middleArr = [];
  }

  // 纵向填表,在图3中从上到下填表,填完一列,再到下一列。
  for (let j = 1; j < strArr.length; j++) {
    for (let i = 0; i < j; i++) {
      // 当s[i] != s[j], 以i为头,j为尾的字符串 肯定不为回文字符串
      if (strArr[i] !== strArr[j]) {
        statusArr[i][j] = false;
      } else {
        // 当第i个元素等于第j个元素时s[i] == s[j]:
          // 当字符串长度小于等于3(即:len = j - i + 1), 则直接为回文字符串
        if (j - i < 3) {
          statusArr[i][j] = true;
        } else {
          // 否则该字符串是否为回文,需要依赖它中间的前一个字符串(即下标i+1 到 j-1)
          statusArr[i][j] = statusArr[i+1][j-1];
        }
      }

      // 记录每次最大长度的回文字符串的头部下标startIndex,以及其长度maxLen
      if (statusArr[i][j] && j - i + 1 > maxLen) {
        maxLen = j - i + 1;
        startIndex = i;
      }
    }
  }
  return str.substring(startIndex, maxLen + startIndex);
}

填完表后,我们可以人为直接在表中看出哪些为回文字符串,并且可以找出最长的回文字符串。

但在代码中,我们是每找到一个回文字符串,就将其与最大字符串比较,替换或丢弃。

这里动态规划解法其实就是利用二维数组来代替两层for循环,减小时间复杂度( O(n^3) => O(n^2) )。

四、Manacher算法  时间复杂度O(n)

看到这个时间复杂度为O(n),就知道这是一个“神仙算法”,其原理就是结合前面的中心扩散与动态规划。

因能力有限,无法分享更多~~

大家如果需要,请查找其他资料,笔者这里推荐一个:

最长回文子串 - 最长回文子串 - 力扣(LeetCode) (leetcode-cn.com)https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/

好了,本章分享到此结束。

如有错误,欢迎指正,后面会继续修改~~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

$陈sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值