LeetCode 5. 最长回文子串 | 算法-从菜鸟开始

简介: 本文是《算法-从菜鸟开始》系列文章的第6篇,欢迎收藏、留言、点赞。话不多说,让我们继续我们的算法之旅。

一、LeetCode 5. 最长回文子串


题目介绍:


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


示例:


输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。


基本思考:什么是回文字符串?


首先在处理这个问题的时候,我们要先考虑什么样的字符串时回文字符串,总结一下特点:


回文字符串长度 示例 特点
1 "a" 一个字符肯定是,不用猜
2 "aa" 两个字符一模一样,肯定是
> 2 "abcba" 从中心点看,左右对称


现在我们假定某个字符串s是回文字符串,该字符串的长度可以使用从索引i到索引j来表示(即从索引i开始到索引j结束的一个字符串)。


/**
 * @method isPalindrome
 * @description 判断字符串s的索引i到索引j是否是回文字符串
 * @param {number} i
 * @param {number} j
 */
const isPalindrome = (i, j) => {
  // 1. 是否是一个字符
  if (i === j) return true;
  // 2. 是否是首尾一致,首尾不一致肯定不是回文字符串
  if (s[i] !== s[j]) return false;
  // 3. 判断索引i和索引j的关系,
  //    基于上面的2条件,走到条件3说明 s[i]和s[j]中间就只有一个字符了,所以这就是一个回文字符串
  if (i + 1 === j) return true;
  // 如果以上条件都不满足,则继续向中间逼近,递归处理
  return isPalindrome(i + 1, j - 1);
}


使用递归的方式来实现,算法的时间复杂度是 O(n)O(n)O(n)


接下来我们思考,如何查询出字符串s中的最长回文子串呢?


老实人的想法:暴力枚举


老实人再次上线,可以枚举出字符串s中所有的子串组合,取最大值即可。


上代码!


/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function(s) {
  /**
   * @param {number} i
   * @param {number} j
   */
  const isPalindrome = (i, j) => {
    // 1. 是否是一个字符
    if (i === j) return true;
    // 2. 是否是首尾一致,首尾不一致肯定不是回文字符串
    if (s[i] !== s[j]) return false;
    // 3. 判断索引i和索引j的关系,
    //    基于上面的2条件,走到条件3说明 s[i]和s[j]中间就只有一个字符了,所以这就是一个回文字符串
    if (i + 1 === j) return true;
    // 如果以上条件都不满足,则继续向中间逼近,递归处理
    return isPalindrome(i + 1, j - 1);
  }
  // 缓存字符串s的长度,不必在执行循环时,每次都计算一次。
  const len = s.length;
  // 设置起始索引indexI
  let indexI = 0;
  // 设置结束索引为indexJ
  let indexJ = 0;
  // 两层循环,将每一种子串都枚举
  for (let i = 0; i < len; i++) {
    for (let j = i + 1; j < len; j++) {
      // 判断是否是回文字符串
      if (isPalindrome(i, j)) {
        // 如果是回文子串,则将当前长度与历史长度进行比较,取二者的最大值
        if (j - i > indexJ - indexI) {
          // 记录此时的i、j
          indexI = i;
          indexJ = j;
        }
      }
    }
  }
  // 注意审题,返回的是回文子串,不是长度
  return s.substring(indexI, indexJ + 1);
};


提交代码!


网络异常,图片无法展示
|


执行结果是:超出时间限制,看起来似乎有问题,也好像没啥问题,实际就是算法效率不高!


再次看下这个算法实现时的时间复杂度是多少?两层for循环是O(n2)O(n^2)O(n2),在加上最里面判断是否是回文字符串的isPalindrome方法O(n)O(n)O(n),最终的复杂度是 O(n3)O(n^3)O(n3)


递归优化经典策略 - 缓存优化


每一个递归的实现都是可以进行缓存优化的,在函数isPalindrome中,我们可以使用缓存cache[i][j]来记录对应ij位置的结果,不必每次都重新计算了。


小小调整下:


// 定义二维数组,缓存结果
const cache = [];
const isPalindrome = (i, j) => {
  // 如果不存在某个一维数组则进行初始化
  if (cache[i] === undefined) {
    cache[i] = [];
  }
  // 如果对应[i][j]存在,则直接返回结果
  if (cache[i][j] !== undefined) {
    return cache[i][j];
  }
  // 1. 是否是一个字符
  if (i === j) return true;
  // 2. 是否是首尾一致,首尾不一致肯定不是回文字符串
  if (s[i] !== s[j]) return false;
  // 3. 判断索引i和索引j的关系,
  //    基于上面的2条件,走到条件3说明 s[i]和s[j]中间就只有一个字符了,所以这就是一个回文字符串
  if (i + 1 === j) return true;
  // 如果以上条件都不满足,则继续向中间逼近,递归处理
  cache[i][j] = isPalindrome(i + 1, j - 1);
  return cache[i][j];
}


再次提交,看看


网络异常,图片无法展示
|


执行结果是通过了,但是时间复杂度还是O(n3)O(n^3)O(n3)性能上还是非常不好看,接下来我们可以尝试换一种角度来看看这个问题。


动态规划看问题


我们假定在索引i和索引j之间是回文字符串,j在前,i在后,使用dp[i][j]来表示。那么dp[i][j]的值相当于中间的回文字符串dp[i - 1][j + 1]结果加上边界值s[i] === s[j]的结果。


看下代码的实现


/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function(s) {
  const len = s.length;
  const dp = [];
  let [ indexJ, indexI ] = [0, 0];
  for (let i = 0; i < len; i++) {
    if (!dp[i]) {
      dp[i] = []
    }
    dp[i][i] = true;
    // 注意是从j到i,so...
    for (j = 0; j < i; j++) {
      // 在这里要注意dp[i - 1][j + 1]的边界问题
      // 如 cbb,当i=2,j=1时,dp[i - 1][j + 1]的值是dp[1][2]是不存在的 undefined,但是bb确实符合回文字符串
      // 所以要确定 dp[i - 1][j + 1]是否是已经存在的值
      dp[i][j] = s[i] === s[j] ? dp[i - 1]?.[j + 1] ?? true  : false;
      // 判断是否是符合条件,并且长度大于历史最大值
      if (dp[i][j] && (i - j) > (indexI - indexJ)) {
        [indexJ, indexI] = [j, i]
      }
    }
  } 
  return s.substring(indexJ, indexI + 1)
};


这里的时间复杂度已经是O(n2)O(n^2)O(n2)了呦


算法-从菜鸟开始,而无止境。与君共勉!


相关文章
|
2天前
|
存储 算法 JavaScript
怎么刷算法,leetcode上有哪些经典题目
怎么刷算法,leetcode上有哪些经典题目
18 0
|
2天前
|
算法 搜索推荐 程序员
第六十六练 最长回文子串 - Manacher算法
第六十六练 最长回文子串 - Manacher算法
3 0
|
2天前
leetcode代码记录(最长回文子串
leetcode代码记录(最长回文子串
8 2
|
2天前
|
存储 算法
Leetcode 30天高效刷数据结构和算法 Day1 两数之和 —— 无序数组
给定一个无序整数数组和目标值,找出数组中和为目标值的两个数的下标。要求不重复且可按任意顺序返回。示例:输入nums = [2,7,11,15], target = 9,输出[0,1]。暴力解法时间复杂度O(n²),优化解法利用哈希表实现,时间复杂度O(n)。
22 0
|
2天前
|
算法
代码随想录算法训练营第六十天 | LeetCode 84. 柱状图中最大的矩形
代码随想录算法训练营第六十天 | LeetCode 84. 柱状图中最大的矩形
23 3
|
2天前
|
存储 算法
代码随想录算法训练营第五十九天 | LeetCode 739. 每日温度、496. 下一个更大元素 I
代码随想录算法训练营第五十九天 | LeetCode 739. 每日温度、496. 下一个更大元素 I
22 1
|
2天前
|
算法
代码随想录算法训练营第五十七天 | LeetCode 739. 每日温度、496. 下一个更大元素 I
代码随想录算法训练营第五十七天 | LeetCode 739. 每日温度、496. 下一个更大元素 I
18 3
|
2天前
|
算法
代码随想录算法训练营第五十六天 | LeetCode 647. 回文子串、516. 最长回文子序列、动态规划总结
代码随想录算法训练营第五十六天 | LeetCode 647. 回文子串、516. 最长回文子序列、动态规划总结
34 1
|
2天前
|
算法
代码随想录算法训练营第五十五天 | LeetCode 583. 两个字符串的删除操作、72. 编辑距离、编辑距离总结
代码随想录算法训练营第五十五天 | LeetCode 583. 两个字符串的删除操作、72. 编辑距离、编辑距离总结
24 1
|
2天前
|
算法 API DataX
二叉树(下)+Leetcode每日一题——“数据结构与算法”“对称二叉树”“另一棵树的子树”“二叉树的前中后序遍历”
二叉树(下)+Leetcode每日一题——“数据结构与算法”“对称二叉树”“另一棵树的子树”“二叉树的前中后序遍历”