给你一个整数数组 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
1. 生活案例:排队挑高挑战
想象有一群人正在排队买奶茶,由于现场太无聊,大家决定玩一个游戏:
- 规则:我们要从这排人中挑出一组人,要求他们的身高是从矮到高排列的(严格递增)。
- 约束:你不能打乱他们的原始站位顺序,只能选择“留着他”或者“跳过他”。
- 目标:找出能组成的最长那一队“身高递增小组”的人数。
例子:人群身高是 [10, 9, 2, 5, 3, 7, 101, 18]
- 如果你选
[2, 3, 7, 18],长度是 4。 - 如果你选
[2, 3, 7, 101],长度也是 4。 - 这就是我们要找的“最长递增子序列”的长度。
2. 代码解析与“生活化”注释
你代码中采用的是 动态规划 (Dynamic Programming) 算法。它的核心思想是:每个人都看看排在他前面的人,找一个比自己矮、且对方身后跟着的小组人数最多的人,然后加入他们。
JavaScript
/**
* @param {number[]} nums - 这一排人的身高数组
* @return {number} - 最长递增小组的人数
*/
var lengthOfLIS = function (nums) {
let n = nums.length;
if (n === 0) return 0; // 如果没人在排队,长度就是0
let maxLen = 1; // 记录全局最长的那个小组长度
// dp[i] 的含义:以第 i 个人结尾时,能组成的“最长递增小组”的人数
// 初始值全为 1,因为每个人哪怕不跟别人组队,自己也能成一个组
let dp = new Array(n).fill(1);
// 外层循环:轮到第 i 个人开始考虑“我该跟在谁后面”
for (let i = 1; i < n; i++) {
// 内层循环:第 i 个人回头看他前面所有的第 j 个人 (0 到 i-1)
for (let j = 0; j < i; j++) {
// 生活化解释:
// 如果第 i 个人发现自己比前面的第 j 个人高 (nums[i] > nums[j])
if (nums[i] > nums[j]) {
// 那么 i 就可以考虑排在 j 的后面
// 他会算一下:如果我跟在 j 后面,我的小组长度就是 dp[j] + 1
// 他会对比各种方案,选择能让他所在小组人数最多的那个
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
// 每次算完一个人的最优选,看看是不是打破了世界记录
maxLen = Math.max(dp[i], maxLen);
}
return maxLen; // 返回最长的小组人数
};
3. 为什么代码要这么写?(底层逻辑)
- 分治与记录:我们不是一次性算出全场最长,而是先算“如果以第 1 个人结尾最长是多少?”、“如果以第 2 个人结尾最长是多少?”以此类推。
- 状态转移:当你算到第 个人的时候,其实你已经知道了前面所有人( 到 )能组成的最长长度。你只需要做一次简单的“比身高”和“加 1”的操作。
- 时间复杂度:因为有两层嵌套循环,你的这份代码复杂度是 。对于长度 2500 的数组来说运行得很快,但如果人再多一点(比如 10 万人),就需要用进阶的 二分查找法 来优化到 了。