【LeetCode热题100道笔记+动画】最长递增子序列

题目描述

给你一个整数数组 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 < = n u m s . l e n g t h < = 2500 1 <= nums.length <= 2500 1<=nums.length<=2500
  • - 10 4 < = n u m s [ i ] < = 10 4 10^4 <= nums[i] <= 10^4 104<=nums[i]<=104

进阶:

  • 你能将算法的时间复杂度降低到 O ( n l o g ( n ) ) O(n log(n)) O(nlog(n)) 吗?

思考一:动态规划

动态规划解法通过定义状态追踪以每个元素结尾的最长递增子序列长度:

  • dp[i] 表示以第 i 个元素结尾的最长递增子序列长度
  • 核心逻辑:对于每个元素,比较其与之前所有元素,若当前元素更大,则可基于之前的子序列延长

算法过程

  1. 初始化

    • dp 数组全部设为 1(每个元素自身构成长度为 1 的子序列)
    • maxLen 初始化为 1(最小可能的最长子序列长度)
  2. 状态更新

    • 遍历每个元素 i(从 1 到 n-1):
      • 遍历其之前的所有元素 j(从 0 到 i-1):
        • nums[i] > nums[j],则 dp[i] = max(dp[i], dp[j] + 1)(基于 j 结尾的子序列延长)
      • 更新 maxLenmax(maxLen, dp[i])(记录全局最长长度)
  3. 返回结果maxLen 即为最长递增子序列的长度

时间复杂度

  • 时间复杂度:O(n²),外层循环遍历 n 个元素,内层循环对每个元素最多比较 n
  • 空间复杂度:O(n),需存储长度为 ndp 数组

该方法通过动态规划暴力枚举所有可能的前序元素,完整覆盖了以每个元素结尾的最长递增子序列情况。

代码

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
    const n = nums.length;
    const dp = Array(n).fill(1);
    let maxLen = 1;
    for (let i = 1; i < n; i++) {
        for (let j = 0; j < i; j++) {
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
        maxLen = Math.max(maxLen, dp[i]);
    }

    return maxLen;
};

思考二:二分查找

传统动态规划(O(n²))会重复比较所有历史元素,而实际上我们只需要关注"对未来扩展最有利"的子序列信息。
"最有利"的定义是:对于长度为k的子序列,其末尾元素越小,未来能拼接的元素就越多

  1. 维护一个辅助数组tails

    • tails[i]表示:所有长度为i+1的递增子序列中,末尾元素的最小值
    • 这个数组天然具有单调性(严格递增)
  2. 对每个新元素执行二分查找

    • tails中找到第一个大于等于当前元素的位置pos
    • pos等于tails长度,说明当前元素可组成更长的子序列,直接追加
    • 否则,用当前元素替换tails[pos],确保同长度子序列的末尾元素最小
  3. 最终结果

    • tails数组的长度就是LIS的长度(不必关心具体子序列内容)

算法过程

  1. 初始化辅助数组:创建空数组tails,用于记录“长度为i+1的递增子序列的最小末尾元素”(tails[i]对应长度i+1)。

  2. 遍历原数组元素:对每个元素nums[i]执行以下操作:

    • 二分查找位置:在tails中查找第一个大于等于nums[i]的位置pos(左右指针l=0r=tails.length-1,通过中间值m调整范围,直至l > r,此时l即为目标pos)。
    • 更新辅助数组
      • pos等于tails长度(当前元素大于tails中所有元素),则将其追加到tails(形成更长的子序列)。
      • 否则,用nums[i]替换tails[pos](确保长度为pos+1的子序列末尾元素最小,为后续扩展留更多可能)。
  3. 返回结果:遍历结束后,tails数组的长度即为最长递增子序列的长度。

时间复杂度

  • 时间复杂度:O(n log n),遍历n个元素,每个元素的二分查找耗时O(log k)(k为当前tails长度,最大为n)。
  • 空间复杂度:O(n),tails数组最长为n(当原数组完全递增时)。

该方法通过维护“最小末尾元素”数组和二分查找,大幅优化了时间效率,是LIS问题的最优解法之一。

代码

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
    const n = nums.length;
    const tail = [];
    for (let i = 0; i < n; i++) {
        let l = 0, r = tail.length-1;
        while (l <= r) {
            const m = l + Math.floor((r-l)/2);
            if (tail[m] < nums[i]) {
                l = m + 1;
            } else {
                r = m -1;
            }
        }

        if (l === tail.length) {
            tail.push(nums[i]);
        } else {
            tail[l] = nums[i];
        }
       
    }

    return tail.length;
};

可视化

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值