leetcode-300. 最长递增子序列

169 阅读3分钟

题目

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/lo…

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

⏰:在此提醒:

首先看题目,自己先试着分析一下,看看有什么思路,再去动手实现一下,最后再看接下来的分析与题解!

这样有助于自己提升自身的思考分析能力,同时能更好的找到自己的问题,快速吸收所需!

快去快回...

image.png

分析及题解

最长严格递增子序列的长度关键字:最长长度(只求最长长度,不用求具体最长子序列)、严格(必须大于,不能等于)、递增子序列

第一种方法:二分查找

  1. 维护一个子序列存放当前的递增序列
  2. 将当前数与子序列最大值比较,如果比最大值大则加入队尾,如果不大于则找一个合适的位置替换当前位置的元素

缺点:其实只能找到最长长度,算是类比出最长长度,占位得到最长长度,但是不能获取到真正的子序列。

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
    let n = nums.length;
    if(n <= 1){
        return n;
    }
    let tail = [nums[0]]; // 递增子序列
    for(let i = 0;i < n;i++){
        if(nums[i] > tail[tail.length-1]){ // 大于则加到队尾
            tail.push(nums[i]);
        }else{ // 不大于情况,找合适位置
            let left = 0;
            let right = tail.length-1;
            while(left < right){
                let mid = (left + right) >> 1;
                if(tail[mid] < nums[i]){ // 二分查找,找合适位置
                    left = mid + 1;
                }else{
                    right = mid;
                }
            }
            tail[left] = nums[i];
        }
    }
    console.log(tail)
    return tail.length;
};

友情赠送:二分查找

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。

二分查找之所以快是因为它只需检查很少几个条目(相对于数组的大小)就能够找到目标元素,或者是确认目标元素不存在。

友情赠送:右移运算符(>>)

定义:将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。

例如:a=a>>2 将a的二进制位右移2位,左补0 或者 左补1得看被移数是正还是负。

操作数每右移一位,相当于该数除以2。

第二个方法:动态规划

  1. 这种问题一般就可以联想到动态规划,那就dp开始。
  2. dp[i]含义:从0到下标为i的序列的最长子序列长度。
  3. 最终结果(子序列的最大长度)应该是 dp 数组中的最大值。

缺点:第二个for循环比较浪费资源,把当前的和之前的全部又比较了一次,算是比较暴力的解法。

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function (nums) {
    const n = nums.length
    if(n<=1) return n
    const dp = new Array(n).fill(1); // 初始化 dp
    for (let i = 0; i < n; i++) {
        // i与i前面的元素比较
        for (let j = 0; j < i; j++) {
            // 找比i小的元素,找到一个,就让当前序列的最长子序列长度加1
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }
    // 找出最大的子序列
    return Math.max(...dp);
};

第三种方法:动态规划+二分查找

  1. 在第二种方法上去优化那个暴力比较那一步。
  2. dp[i]含义:从0到下标为i的序列的最长子序列尾部的最小值。
  3. dp[0]没用,设置为null;dp[1]初始值nums[0]。
  4. 如果nums[i] 大于 dp中所有元素,则加入dp尾部;如果不大于则覆盖掉比它大的那个元素。
/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function (nums) {
    const n = nums.length
    if(n<=1) return n
    const dp = [null, nums[0]]; // 初始化 dp
    let max = 1
    for (let i = 1; i < n; i++) { // 从1开始
        // i与dp 最大的元素比较
        if(nums[i]>dp[max]) { // 大于,加入尾部
            dp[++max] = nums[i]
        }else{ // 小于,找位置
            let left = 1, right = max, mid, pos = 0
            while(left<=right){ // 二分查找
                mid = (left+right) >> 1
                if(nums[i]>dp[mid]) { // 在右边
                    left = mid + 1
                    pos = mid
                }else{// 在左边
                    right = mid -1
                }
            }
            dp[pos+1] = nums[i]
        }
    }
    console.log(dp)
    return max
};

似乎和第一种方法比较接近。哈哈。。。

还有很多比较好的方法,如果大家有更优解,可以在评论区留言附上代码,大家一同进步!加油⛽️!