[路飞]_leetcode刷题_300. 最长递增子序列

141 阅读3分钟

「这是我参与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里放入,判断

  1. 如果nums[i]大于arr最后一项,则直接push
  2. 如果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
};