算法 - 最长递增子序列 (LIS)
1. 题目
给你一个整数数组 nums,取出其中严格递增的最长子序列,返回其长度。子序列不要求连续,但要保持相对顺序。
示例:
1 | 输入: nums = [10, 9, 2, 5, 3, 7, 101, 18] |
2. 解题思路
2.1 动态规划 O(n²)
定义 dp[i] = 以 nums[i] 结尾的最长递增子序列长度。
对每个 i,回看所有 j < i:若 nums[j] < nums[i],则可以把 nums[i] 接在以 j 结尾的序列后面:
1 | dp[i] = max(dp[j] + 1) 对所有 nums[j] < nums[i] |
初始每个 dp[i] = 1(自己单独成列)。答案是 max(dp),注意不是 dp[n-1](结尾不一定是全局最长)。
2.2 贪心 + 二分 O(n log n)(最优)
维护数组 tails:tails[k] 表示「长度为 k+1 的递增子序列的最小可能结尾」。直觉:同样长度下,结尾越小越容易再接更大的数,越有潜力。
遍历 nums 中每个 x:
- 用二分在
tails中找第一个>= x的位置:- 找到 → 用
x替换它(让这个长度的结尾更小)。 - 没找到(
x比所有都大)→ push 到末尾,LIS 长度 +1。
- 找到 → 用
tails的长度即为答案。
注意:
tails本身不是某个真实的子序列,只是长度正确;不要试图从中还原路径。
- 时间复杂度:
O(n log n);空间复杂度:O(n)。
3. TypeScript 实现
1 | // 2.1 DP:O(n²) |
4. 面试延伸
- 非严格递增(允许相等):把二分的
lowerBound换成upperBound(找第一个> x)。一个字符之差,常考。 - 俄罗斯套娃信封(354):先按宽升序、宽相同按高降序排序,再对高求 LIS,是 LIS 的二维包装。
- 最长递增子序列的个数(673):在 DP 里再维护一个
count[]数组。 - 要求输出具体子序列:只能用 O(n²) DP +
parent[]回溯,贪心二分法无法直接还原(可另存下标)。 - 面试写二分要能讲清
lo的含义(第一个>= x),否则容易 off-by-one。
难度:中等 | LeetCode 300 题 | DP + 二分双考点