LeetCode 最长递增子序列(贪心 二分 | 线性DP 动态规划)对比

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. 核心逻辑
  • 贪心策略:维护数组 tailstails[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后,进一步优化时间复杂度。

五、核心结论

  1. 工程首选 贪心 + 二分查找:时间效率最优,适用于绝大多数仅需长度的场景。
  2. 线性DP作为基础工具:适合入门学习,且在需要输出具体子序列时不可替代。
  3. 两者空间复杂度一致,核心差异在于时间效率和功能扩展性,需按需选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值