「这是我参与2022首次更文挑战的第22天,活动详情查看:2022首次更文挑战」
题目
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
解法一
动态规划
思路
假设dp[i]代表,nums数组从0到下标为i的数能构成的最长子序列的长度。 假设[0,i-1]中能构成子序列最长的为j,那么dp[j] = Math.max(dp[0],...dp[i-1])
- 如果nums[i] > nums[j],则dp[i] 就应该等于 dp[j] + 1;
- 如果nums[i] <= nums[j],则dp[i] == dp[j]
且有,dp[0] = 1,即下标为0的数能构成的最长子序列为1。
这样的话,动态规划的两个点都很明确了
- 边界:dp[0] = 1
- 最优子结构:即dp[i]是可以由它之前的子问题推导出来
- 状态转移方程:即dp[i] = dp[j] 或 dp[i] = dp[j]+1
代码
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function(nums) {
if(nums.length == 0){
return 0;
}
let dp = [];
dp[0] = 1;
let maxlen = 1;
for(let i=1;i<nums.length;i++){
dp[i] = 1;
for(let j=0;j<i;j++){
if(nums[j]<nums[i]){
dp[i] = Math.max(dp[i],dp[j]+1)
}
}
maxlen = Math.max(dp[i],maxlen)
}
return maxlen;
};
复杂度分析
时间复杂度:O(n2),双循环。
空间复杂度:O(n),需要一个dp数组去存取每一个下标值对应的最长子序列。
解法二
贪心 + 遍历查找
思路
遍历数组nums,每次拿出一个nums[i]往一空数组arr里放入,判断
- 如果nums[i]大于arr最后一项,则直接push
- 如果nums[i]小于arr最后一项,则去arr里寻找,看谁比nums[i]小,找到则将该项的后一项替换成nums[i]
判断1很好理解,如果大于最后一项,则直接推入能直接加上子序列长度 判断2,这么做是因为,我们当前项nums[i] < arr[len-1], 那么nums[i]虽然无法加上当前子序列的长度,但是将它放到合适的位置,不会影响当前子序列的后序新增,并且可以让子序列整体变小,那么后面再能接受的数就能更多。
企业级理解:每次来一个新员工,如果要价比所有老员工都高,那么直接加入团队。如果要价比老员工低,则替换掉那个价格和他最接近且比他要价高的老员工。这样技能保证团队可以稳定运行,还能降低成本。
代码
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function(nums) {
let arr = [];
for(let i=0;i<nums.length;i++){
if(nums[i]>arr[arr.length-1]){
arr.push(nums[i])
} else if(nums[i]<arr[arr.length-1]) {
for(let j=arr.length-1;j>=0;j--){
if(nums[i]>arr[j]){
arr[j+1] = nums[i]
break;
} else if( j==0 && nums[i] < arr[j]){
arr[j] = nums[i]
break;
}
}
}else if(arr[arr.length-1] == undefined ){
arr.push(nums[i])
}
}
return arr.length
};
复杂度分析
时间复杂度:O(n2),也是遍历每一项,都要去将前面所有项都再遍历一遍。
空间复杂度:O(n),如要一个新数组来存放数据
解法三
贪心 + 二分查找
思路
在解法二的基础上,我们可以优化查找方式,因为arr已经是有序的了,我们可以使用二分查找,那每次即可减少一半的查找数,能大幅减少时间复杂度。
代码
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function(nums) {
let arr = [];
for(let i=0;i<nums.length;i++){
if(nums[i]>arr[arr.length-1]){
arr.push(nums[i])
} else if(nums[i]<arr[arr.length-1]) {
let left = 0;
let right = arr.length-1;
let pos = -1;
while(left<=right){
let mid = Math.floor((right-left)/2) + left;
if(nums[i]>arr[mid]){
left = mid+1;
pos = mid;
}else{
right = mid-1
}
}
arr[pos+1] = nums[i];
}else if(arr[arr.length-1] == undefined ){
arr.push(nums[i])
}
}
return arr.length
};
复杂度分析
时间复杂度:O(nlogn),遍历为n,查找为logn。
空间复杂度:O(n)
解法四
解法三的优化写法
思路
其实和解法三思路是一样的,解法三的代码通俗易懂,但不够精简,这里将它再精简一点。
代码
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function(nums) {
let len = 1;
let d = [];
d[len] = nums[0];
for(let i=1;i<nums.length;i++){
if(nums[i]>d[len]){
d.push(nums[i]);
len++;
}else{
let left = 1;
let right = len;
let pos = 0;
while(left<=right){
let mid = Math.floor((right-left)/2)+left;
if(nums[i]>d[mid]){
left = mid+1;
pos = mid;
}else{
right = mid-1
}
}
d[pos+1] = nums[i]
}
}
return len
};