最长递增子序列(题号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)) 吗?
链接
解释
这题啊,这题也能勉强做出来,可是想法上还是有些问题的。
很明显可以知道这题是动态规划,因为涉及到重复的状态,就是一个状态可能会用到多次,如果存储下来就可以避免下次再进行调用,这也就是动态规划的核心:递归(循环)+记忆化。
首先看看题目,最长递增子序列,那么有点类似类似于之前的路径有多种走法,需要先知道前面的最大子序列才能知道下一个元素的最大子序列。
确切的说,并不是前一个单位的最大子序列,而是前面所有单位中,小于当前元素的大小的拥有最大子序列元素,只要找到这个元素再加一,就是当前元素的最大递增子序列。
那么首先需要循环一下数组,这一层是必不可少的,那么还需要再内部再进行一次循环,这次循环的长度是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
为空,直接插入10
,lts
为[10]
- 此时
- 第二次循环,元素为
9
- 由于
9
比10
小,那么直接将10
替换成9
,lts
为[9]
- 由于
- 第三次循环,元素为
2
- 由于
2
比9
小,那么将9
替换成2
,lts
为[2]
- 由于
- 第四次循环,元素为
5
- 由于
5
比2
大,那么直接将5
添加到数组末尾,lts
为[2, 5]
- 由于
- 第五次循环,元素为
3
- 由于
3
比5
大,比2
小,将5
替换成3
,lts
为[2, 3]
- 由于
- 第六次循环,元素为
7
- 由于
7
比3
大,那么直接将7
添加到数组末尾,lts
为[2, 3, 7]
- 由于
- 第七次循环,元素为
101
- 由于
101
比7
大,那么直接将101
添加到数组末尾,lts
为[2, 3, 7, 101]
- 由于
- 第八次循环,元素为
18
- 由于
18
比7
大,比101
小,将101
替换成18
,lts
为[2, 3, 7, 18]
- 由于
- 第九次循环,元素为
20
- 由于
20
比18
大,那么直接将20
添加到数组末尾,lts
为[2, 3, 7, 18, 20]
- 由于
在循环中完成后会发现lts
数组的长度为5,最后直接返回5
即可。
具体的代码部分这里就不多做赘述了,因为确实很难想到,被问到应该也想不出这种二分查找的方法,👇放个链接,以作参考:[这里](
PS:想查看往期文章和题目可以点击下面的链接:
这里是按照日期分类的👇
经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇
有兴趣的也可以看看我的个人主页👇