Problem: 300. 最长递增子序列
整体思路
这段代码同样旨在解决 “最长递增子序列” (LIS) 问题。它采用的是一种经典的 自底向上(Bottom-Up)的动态规划 方法,也称为 迭代法 或 制表法 (Tabulation)。这是解决 LIS 问题的标准 O(N^2) 解法。
算法的核心思想是通过构建一个DP表(dp
数组),从左到右依次计算出以每个元素为结尾的最长递增子序列的长度。
-
状态定义:
- 算法定义了一个DP数组
dp
。 dp[i]
的含义是:在nums
数组中,以nums[i]
为结尾的最长递增子序列的长度。这个定义与记忆化搜索版本中的dfs(i)
完全一致。
- 算法定义了一个DP数组
-
基础情况 (Base Case):
- 对于任何
dp[i]
,其最基本的情况是子序列只包含nums[i]
本身,此时长度为 1。代码通过在循环最后dp[i]++
来确保每个dp[i]
的值至少为1。
- 对于任何
-
状态转移 (State Transition):
- 算法使用两层嵌套循环来填充
dp
数组。 - 外层循环
for (int i = 0; i < n; i++)
:这个循环负责计算dp[i]
的值,即从dp[0]
逐步计算到dp[n-1]
。它遍历所有可能的子序列结尾。 - 内层循环
for (int j = 0; j < i; j++)
:对于当前要计算的dp[i]
,这个循环会回顾i
之前的所有位置j
。 - 转移条件
if (nums[j] < nums[i])
:- 这个条件检查
nums[i]
是否可以接在以nums[j]
结尾的递增子序列的后面。 - 如果可以,那么通过连接
nums[i]
,我们就形成了一个新的、更长的以nums[i]
结尾的递增子序列,其长度为dp[j] + 1
。 dp[i] = Math.max(dp[i], dp[j])
:这行代码是在寻找所有可能的“前导”子序列中,那个最长的。它将dp[i]
更新为所有满足条件的dp[j]
中的最大值。
- 这个条件检查
- 最终计算:内层循环结束后,
dp[i]
存储了max(dp[j])
的值。然后通过dp[i]++
,将nums[i]
本身计入长度,完成了dp[i] = 1 + max(dp[j])
的计算。
- 算法使用两层嵌套循环来填充
-
寻找全局最大值:
dp[i]
只代表以nums[i]
为结尾的 LIS 长度,不一定是全局最长的。- 全局最长的递增子序列必然以某个
nums[i]
结尾。 - 因此,在每次计算完一个
dp[i]
后,都需要用它来更新全局最大值ans
。
-
返回结果:
- 所有循环结束后,
ans
中就存储了全局的最长递增子序列的长度。
- 所有循环结束后,
完整代码
class Solution {
/**
* 计算数组中最长递增子序列的长度。
* @param nums 输入的整数数组
* @return 最长递增子序列的长度
*/
public int lengthOfLIS(int[] nums) {
int n = nums.length;
// dp 数组:dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度。
int[] dp = new int[n];
// ans: 用于存储全局的最长递增子序列长度。
int ans = 0;
// 外层循环:遍历所有可能的结尾位置 i,计算 dp[i]。
for (int i = 0; i < n; i++) {
// 内层循环:在 i 之前寻找可以连接的前导子序列。
for (int j = 0; j < i; j++) {
// 如果 nums[j] < nums[i],说明 nums[i] 可以接在以 nums[j] 结尾的 LIS 后面。
if (nums[j] < nums[i]) {
// 我们希望找到最长的那个前导子序列。
// dp[i] 在这里暂时存储 max(dp[j])。
dp[i] = Math.max(dp[i], dp[j]);
}
}
// 将 nums[i] 本身计入长度。
// 此时 dp[i] = 1 + max(dp[j]),完成状态转移。
// 如果内层循环从未执行(i=0),dp[i] 初始为0,dp[i]++后为1,正确。
dp[i]++;
// 用刚计算出的 dp[i] 更新全局最大值 ans。
ans = Math.max(ans, dp[i]);
}
// 返回最终的全局最大长度。
return ans;
}
}
时空复杂度
时间复杂度:O(N^2)
- 循环结构:算法使用了两层嵌套的
for
循环。- 外层循环从
i = 0
运行到n-1
,执行N
次。 - 内层循环从
j = 0
运行到i-1
。其执行次数依赖于i
。
- 外层循环从
- 计算依据:
- 总的操作次数(主要是比较和
Math.max
)大致等于0 + 1 + 2 + ... + (N-1)
。 - 这是一个等差数列求和,结果为
N * (N - 1) / 2
。 - 因此,总的操作次数与
N^2
成正比。
- 总的操作次数(主要是比较和
综合分析:
算法的时间复杂度为 O(N^2)。
空间复杂度:O(N)
- 主要存储开销:算法创建了一个名为
dp
的整型数组来存储动态规划的所有中间状态。 - 空间大小:该数组的长度与输入数组
nums
的长度N
相同。 - 其他变量:
n
,ans
,i
,j
等变量都只占用常数级别的空间,即 O(1)。 - 迭代实现:与记忆化搜索不同,这种迭代方法没有递归调用栈的开销。
综合分析:
算法所需的额外空间主要由 dp
数组决定。因此,其空间复杂度为 O(N)。
参考灵神