LeetCode第300题_最长递增子序列

LeetCode 第300题:最长递增子序列

文章摘要

本文详细解析LeetCode第300题"最长递增子序列",这是一道考察动态规划和二分查找的中等难度题目。文章提供了动态规划和贪心+二分查找两种实现方案,包含C#、Python、C++三种语言实现,配有详细的算法分析和性能对比。适合学习动态规划和二分查找的读者。

核心知识点: 动态规划、二分查找、贪心算法
难度等级: 中等
推荐人群: 具备基础算法知识,想要提升动态规划和二分查找能力的开发者

题目描述

给你一个整数数组 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. 动态规划法:

    • 定义dp[i]为以nums[i]结尾的最长递增子序列长度
    • 对每个位置i,遍历前面的所有位置j
    • 如果nums[i] > nums[j],则可以接在j后面
    • 时间复杂度O(n²)
  2. 贪心+二分查找法:

    • 维护一个递增子序列tails
    • tails[i]表示长度为i+1的递增子序列的最小结尾值
    • 对每个数字二分查找插入位置
    • 时间复杂度O(nlogn)

图解思路

动态规划分析表

位置 当前数字 dp值 更新过程
0 10 1 初始化为1
1 9 1 小于前值,独立为1
2 2 1 最小值,独立为1
3 5 2 可接在2后面
4 3 2 可接在2后面
5 7 3 可接在3或5后面
6 101 4 可接在7后面
7 18 4 可接在7后面

二分查找步骤表

步骤 操作 目的
1 初始化tails数组 存储各长度子序列的最小结尾
2 二分查找位置 找到当前数字的插入位置
3 更新tails 维护最优解
4 返回长度 获取最终结果

代码实现

C# 实现

public class Solution {
    // 动态规划解法
    public int LengthOfLIS(int[] nums) {
        if (nums == null || nums.Length == 0) return 0;
        
        int[] dp = new int[nums.Length];
        int maxLen = 1;
        
        // 初始化dp数组
        Array.Fill(dp, 1);
        
        // 动态规划过程
        for (int i = 1; i < nums.Length; i++) {
            for (int 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;
    }
    
    // 贪心+二分查找解法
    public int LengthOfLIS2(int[] nums) {
        if (nums == null || nums.Length == 0) return 0;
        
        int[] tails = new int[nums.Length];
        int size = 0;
        
        foreach (int num in nums) {
            int left = 0, right = size;
            while (left < right) {
                int mid = left + (right - left) / 2;
                if (tails[mid] < num) {
                    left = mid + 1;
                } else {
                    right = mid;
                }
            }
            tails[left] = num;
            if (left == size) size++;
        }
        
        return size;
    }
}

Python 实现

class Solution:
    # 动态规划解法
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        
        dp = [1] * len(nums)
        
        for i in range(1, len(nums)):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
        
        return max(dp)
    
    # 贪心+二分查找解法
    def lengthOfLIS2(self, nums: List[int]) -> int:
        if not nums:
            return 0
        
        tails = []
        
        for num in nums:
            if not tails or num > tails[-1]:
                tails.append(num)
            else:
                left, right = 0, len(tails)
                while left < right:
                    mid = (left + right) // 2
                    if tails[mid] < num:
                        left = mid + 1
                    else:
                        right = mid
                tails[left] = num
        
        return len(tails)

C++ 实现

class Solution {
public:
    // 动态规划解法
    int lengthOfLIS(vector<int>& nums) {
        if (nums.empty()) return 0;
        
        vector<int> dp(nums.size(), 1);
        int maxLen = 1;
        
        for (int i = 1; i < nums.size(); i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
            maxLen = max(maxLen, dp[i]);
        }
        
        return maxLen;
    }
    
    // 贪心+二分查找解法
    int lengthOfLIS2(vector<int>& nums) {
        if (nums.empty()) return 0;
        
        vector<int> tails;
        
        for (int num : nums) {
            if (tails.empty() || num > tails.back()) {
                tails.push_back(num);
            } else {
                auto it = lower_bound(tails.begin(), tails.end(), num);
                *it = num;
            }
        }
        
        return tails.size();
    }
};

执行结果

C# 实现

  • 执行用时:92 ms (动态规划)
  • 执行用时:88 ms (二分查找)
  • 内存消耗:36.2 MB

Python 实现

  • 执行用时:3644 ms (动态规划)
  • 执行用时:76 ms (二分查找)
  • 内存消耗:15.2 MB

C++ 实现

  • 执行用时:276 ms (动态规划)
  • 执行用时:8 ms (二分查找)
  • 内存消耗:10.4 MB

性能对比

语言 方法 执行用时 内存消耗 特点
C# 动态规划 92 ms 36.2 MB 实现简单,性能一般
C# 二分查找 88 ms 36.2 MB 性能较好
Python 动态规划 3644 ms 15.2 MB 性能较差
Python 二分查找 76 ms 15.2 MB 性能提升明显
C++ 动态规划 276 ms 10.4 MB 性能中等
C++ 二分查找 8 ms 10.4 MB 性能最优

代码亮点

  1. 提供两种实现方案,适应不同场景
  2. 使用二分查找优化时间复杂度
  3. 空间复杂度优化
  4. 代码结构清晰易懂

常见错误分析

  1. 忘记处理空数组情况
  2. 动态规划初始化错误
  3. 二分查找边界处理错误
  4. 更新最大长度时机错误

解法对比

解法 时间复杂度 空间复杂度 优点 缺点
动态规划 O(n²) O(n) 思路直观,易于理解 时间复杂度高
贪心+二分 O(nlogn) O(n) 时间复杂度优,性能好 实现较复杂

相关题目

  • LeetCode 673. 最长递增子序列的个数 - 中等
  • LeetCode 354. 俄罗斯套娃信封问题 - 困难
  • LeetCode 674. 最长连续递增序列 - 简单

系列导航

算法专题合集 - 查看完整合集

关注合集更新:点击上方合集链接,关注获取最新题解!目前已更新第300题。

互动交流

感谢大家耐心阅读到这里!希望这篇题解能够帮助你更好地理解和掌握这道算法题。

如果这篇文章对你有帮助,请:

  • 点个赞,让更多人看到这篇文章
  • 收藏文章,方便后续查阅复习
  • 关注作者,获取更多高质量算法题解
  • 评论区留言,分享你的解题思路或提出疑问

你的支持是我持续分享的动力!

一起进步:算法学习路上不孤单,欢迎一起交流学习!

你可能感兴趣的:(算法,leetcode,算法,职场和发展,学习,c#,游戏,python)