前端刷题路-Day24:最长递增子序列(题号300)

239 阅读3分钟

最长递增子序列(题号300)

题目

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组[0,3,1,6,2,2,7]的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]  
输出:4  
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。  

示例 2:

输入:nums = [0,1,0,3,2,3]  
输出:4  

示例 3:

输入:nums = [7,7,7,7,7,7,7]  
输出:1  

提示:

  • 1 <= nums.length <= 2500
  • -104 <= nums[i] <= 104

进阶:

  • 你可以设计时间复杂度为 O(n²) 的解决方案吗?
  • 你能将算法的时间复杂度降低到 O(nlog(n)) 吗?

链接

leetcode-cn.com/problems/lo…

解释

这题啊,这题也能勉强做出来,可是想法上还是有些问题的。

很明显可以知道这题是动态规划,因为涉及到重复的状态,就是一个状态可能会用到多次,如果存储下来就可以避免下次再进行调用,这也就是动态规划的核心:递归(循环)+记忆化。

首先看看题目,最长递增子序列,那么有点类似类似于之前的路径有多种走法,需要先知道前面的最大子序列才能知道下一个元素的最大子序列。

确切的说,并不是前一个单位的最大子序列,而是前面所有单位中,小于当前元素的大小的拥有最大子序列元素,只要找到这个元素再加一,就是当前元素的最大递增子序列。

那么首先需要循环一下数组,这一层是必不可少的,那么还需要再内部再进行一次循环,这次循环的长度是0到i的长度,所以时间复杂度就是O(n²)。

关于另外一种O(nlogn)解法自己是真的想不出来了,放在更好的方法里进行解释。

自己的答案(动态规划)

var lengthOfLIS = function(nums) {
  var obj = new Map()
      arr = []
      max = 0
  for (let i = nums.length - 1; i > -1; i--) {
    var res = 1
    for (let j = 0; j < arr.length; j++) {
      if (nums[i] < arr[j]) {
        res = Math.max(res, 1 + obj.get(arr[j]))
      }
    }
    obj.set(nums[i], res)
    arr.push(nums[i])
    max = Math.max(max, res)
  }
  return max
};

代码看起来有点诡异,毕竟是自己写的。

当时脑子没转过弯来,想着从后往前循环的,因为是递增子序列,所以从大到小进行排序,后来看了别人的答案后才发现正序循环也是一样的,没什么区别。

而且虽然这里用到了DP,但是DP的存储有点过于复杂,显示用一个对象来存储每个元素的最大子序列个数,之后用一个数组来存储已经走过的节点,也就是arr

这样每次只需要循环arr就能得到了当前节点的最大子序列的个数了,也就是解释中说到的第二层循环。

也是自己比较蠢,没想到其实可以完全不需要对象,直接利用数组的index和数组的元素就完全够了,这么做简直就是脱裤子放屁,更好的解法可以看👇:

更好的方法(动态规划)

var lengthOfLIS = function(nums) {
  var dp = new Array(nums.length).fill(1)
      max = 1
  for (let i = 1; i < nums.length; i++) {
    for (let j = 0; j < i; j++) {
      if (nums[i] > nums[j]) {
        dp[i] = Math.max(dp[i], dp[j] + 1)
      }
    }
    max = Math.max(max, dp[i])
  }
  return max
};

这个动态规划就很简单了,只有一个DP数组用来存储已经走过的节点,然后用第二层循环来确定要循环的DP数组的长度,也就是自己答案中的arr

之后持续循环dp中当前元素的最大值,就可以拿到当前元素的最大子序列的个数了,灰常简单。

更好的方法(二分查找)

这个想法笔者想破脑袋都想不出来这种答案的,看了看题解,也就只有一位老哥拿JavaScript给出了这种答案,剩下的全是动态规划。

原理是这样的,首先最外层的循环是不可避免的,因为必须拿到每个元素来进行比较。

之后需要维护一个公用的二分查找的数组,用来存储这里的最长子序列,每次循环的时候更新这个最长递增子序列,那么循环到最后就可以拿到这个最长递增子序列了,这么说可能不太清楚,举个🌰:

假定数组是:[10,9,2,5,3,7,101,18,20](比原题多个20),维护的子序列是lts

我们开始循环数组:

  • 第一次循环,元素为10
    • 此时lts为空,直接插入10lts[10]
  • 第二次循环,元素为9
    • 由于910小,那么直接将10替换成9lts[9]
  • 第三次循环,元素为2
    • 由于29小,那么将9替换成2lts[2]
  • 第四次循环,元素为5
    • 由于52大,那么直接将5添加到数组末尾,lts[2, 5]
  • 第五次循环,元素为3
    • 由于35大,比2小,将5替换成3lts[2, 3]
  • 第六次循环,元素为7
    • 由于73大,那么直接将7添加到数组末尾,lts[2, 3, 7]
  • 第七次循环,元素为101
    • 由于1017大,那么直接将101添加到数组末尾,lts[2, 3, 7, 101]
  • 第八次循环,元素为18
    • 由于187大,比101小,将101替换成18lts[2, 3, 7, 18]
  • 第九次循环,元素为20
    • 由于2018大,那么直接将20添加到数组末尾,lts[2, 3, 7, 18, 20]

在循环中完成后会发现lts数组的长度为5,最后直接返回5即可。

具体的代码部分这里就不多做赘述了,因为确实很难想到,被问到应该也想不出这种二分查找的方法,👇放个链接,以作参考:[这里](



PS:想查看往期文章和题目可以点击下面的链接:

这里是按照日期分类的👇

前端刷题路-目录(日期分类)

经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇

前端刷题路-目录(题型分类)

有兴趣的也可以看看我的个人主页👇

Here is RZ