LeetCode 最长递增子序列(贪心 二分 | 线性DP 动态规划)对比
一、原问题
给定整数数组 nums,找到其中最长的严格递增子序列的长度(子序列不要求连续)。
示例:输入 [10,9,2,5,3,7,101,18],输出 4(最长子序列为 [2,3,7,101] 或 [2,3,7,18])。
二、两种解法核心对比
解法一:线性动态规划(DP)
1. 核心逻辑
- 状态定义:
dp[i]表示以nums[i]为结尾的最长递增子序列长度。 - 状态转移方程:遍历所有
j < i,若nums[j] < nums[i],则dp[i] = max(dp[i], dp[j] + 1)。 - 初始状态:
dp[i] = 1(每个元素自身可构成长度为1的子序列)。 - 结果获取:遍历
dp数组取最大值,即为LIS长度。
2. 时间与空间复杂度
- 时间复杂度:
O(n²)(双层循环,外层遍历n个元素,内层遍历每个元素的前序元素)。 - 空间复杂度:
O(n)(仅需额外维护长度为n的dp数组)。
3. Java代码
class Solution {
public int lengthOfLIS(int[] nums) {
// 边界条件:数组为空或长度为0,返回0
if (nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
// dp数组:dp[i]表示以nums[i]结尾的最长递增子序列长度
int[] dp = new int[n];
// 初始化:每个元素自身是长度为1的子序列
for (int i = 0; i < n; i++) {
dp[i] = 1;
}
int maxLen = 1; // 记录全局最长长度,初始为1(至少有一个元素)
// 遍历每个元素,计算dp[i]
for (int i = 1; i < n; i++) {
// 遍历i之前的所有元素j,寻找能形成递增的子序列
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
// 状态转移:dp[i] = 之前最大值 + 1
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
// 更新全局最长长度
maxLen = Math.max(maxLen, dp[i]);
}
return maxLen;
}
}
解法二:贪心 + 二分查找
1. 核心逻辑
- 贪心策略:维护数组
tails,tails[i]表示长度为i+1的递增子序列的最小末尾元素(确保子序列末尾尽可能小,为后续元素留足递增空间)。 - 二分查找作用:遍历
nums时,快速定位元素在tails中的位置:- 若元素大于
tails最后一个元素,直接追加(子序列长度+1); - 否则,替换
tails中第一个大于等于该元素的位置(优化当前长度子序列的末尾)。
- 若元素大于
- 结果获取:
tails数组的有效长度即为LIS长度(注意:tails本身不是LIS,仅长度一致)。
2. 时间与空间复杂度
- 时间复杂度:
O(n log n)(遍历n个元素,每个元素对应一次二分查找,时间为O(log n))。 - 空间复杂度:
O(n)(维护长度为n的tails数组,最坏情况与原数组长度一致)。
3. Java代码
class Solution {
public int lengthOfLIS(int[] nums) {
// 边界条件:数组为空或长度为0,直接返回0
if (nums == null || nums.length == 0) {
return 0;
}
// tails数组:tails[i]表示长度为i+1的递增子序列的最小末尾元素
int[] tails = new int[nums.length];
int len = 0; // 记录tails的有效长度(即当前最长递增子序列长度)
for (int num : nums) {
// 二分查找:在tails[0..len)中找第一个 >= num的位置
int left = 0, right = len;
while (left < right) {
int mid = left + (right - left) / 2; // 避免整数溢出
if (tails[mid] < num) {
left = mid + 1; // 目标在右半区
} else {
right = mid; // 目标在左半区(包括mid)
}
}
// 替换或追加
tails[left] = num;
// 如果left等于当前len,说明是追加,长度+1
if (left == len) {
len++;
}
}
return len;
}
}
三、优缺点详细对比
| 对比维度 | 线性动态规划(DP) | 贪心 + 二分查找 |
|---|---|---|
| 逻辑直观性 | 优点:思路简单易懂,符合DP常规思维,易上手 | 缺点:思路较抽象,需理解贪心+二分的核心逻辑 |
| 时间效率 | 缺点:O(n²) 效率低,n大时易超时 | 优点:O(n log n) 效率极高,最优解 |
| 空间效率 | 优点:O(n),无额外空间开销 | 缺点:O(n),无优势,tails数组意义不直观 |
| 功能扩展性 | 优点:可回溯输出具体的最长递增子序列 | 缺点:仅能求长度,输出子序列需额外处理 |
| 代码实现难度 | 优点:代码简洁,无复杂边界处理 | 缺点:二分查找边界易出错,需精准控制 |
四、适用场景
1. 线性DP解法适用场景
- 算法入门学习:理解LIS核心逻辑,掌握DP状态定义与转移思想。
- 小规模数据场景:n较小(如n≤1e3),对时间效率要求不高。
- 需要输出具体子序列:题目要求返回最长递增子序列本身时,DP是更优选择。
2. 贪心 + 二分解法适用场景
- 大规模数据场景:n较大(如n≥1e4),需应对LeetCode严格测试用例。
- 仅需获取LIS长度:无需输出具体子序列,追求最优时间效率。
- 算法优化场景:掌握DP后,进一步优化时间复杂度。
五、核心结论
- 工程首选 贪心 + 二分查找:时间效率最优,适用于绝大多数仅需长度的场景。
- 线性DP作为基础工具:适合入门学习,且在需要输出具体子序列时不可替代。
- 两者空间复杂度一致,核心差异在于时间效率和功能扩展性,需按需选择。

1005

被折叠的 条评论
为什么被折叠?



