本文详细解析LeetCode第300题"最长递增子序列",这是一道考察动态规划和二分查找的中等难度题目。文章提供了动态规划和贪心+二分查找两种实现方案,包含C#、Python、C++三种语言实现,配有详细的算法分析和性能对比。适合学习动态规划和二分查找的读者。
核心知识点: 动态规划、二分查找、贪心算法
难度等级: 中等
推荐人群: 具备基础算法知识,想要提升动态规划和二分查找能力的开发者
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
输入:nums = [0,1,0,3,2,3]
输出:4
输入:nums = [7,7,7,7,7,7,7]
输出:1
本题可以使用两种方法来实现:
动态规划法:
贪心+二分查找法:
位置 | 当前数字 | 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 | 返回长度 | 获取最终结果 |
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;
}
}
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)
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 | 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 | 性能最优 |
解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
动态规划 | O(n²) | O(n) | 思路直观,易于理解 | 时间复杂度高 |
贪心+二分 | O(nlogn) | O(n) | 时间复杂度优,性能好 | 实现较复杂 |
算法专题合集 - 查看完整合集
关注合集更新:点击上方合集链接,关注获取最新题解!目前已更新第300题。
感谢大家耐心阅读到这里!希望这篇题解能够帮助你更好地理解和掌握这道算法题。
如果这篇文章对你有帮助,请:
你的支持是我持续分享的动力!
一起进步:算法学习路上不孤单,欢迎一起交流学习!