【LeetCode 热题 100】300. 最长递增子序列——(解法二)自底向上

Problem: 300. 最长递增子序列

整体思路

这段代码同样旨在解决 “最长递增子序列” (LIS) 问题。它采用的是一种经典的 自底向上(Bottom-Up)的动态规划 方法,也称为 迭代法制表法 (Tabulation)。这是解决 LIS 问题的标准 O(N^2) 解法。

算法的核心思想是通过构建一个DP表(dp数组),从左到右依次计算出以每个元素为结尾的最长递增子序列的长度。

  1. 状态定义

    • 算法定义了一个DP数组 dp
    • dp[i] 的含义是:nums 数组中,以 nums[i] 为结尾的最长递增子序列的长度。这个定义与记忆化搜索版本中的 dfs(i) 完全一致。
  2. 基础情况 (Base Case)

    • 对于任何 dp[i],其最基本的情况是子序列只包含 nums[i] 本身,此时长度为 1。代码通过在循环最后 dp[i]++ 来确保每个 dp[i] 的值至少为1。
  3. 状态转移 (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]) 的计算。
  4. 寻找全局最大值

    • dp[i] 只代表以 nums[i] 为结尾的 LIS 长度,不一定是全局最长的。
    • 全局最长的递增子序列必然以某个 nums[i] 结尾。
    • 因此,在每次计算完一个 dp[i] 后,都需要用它来更新全局最大值 ans
  5. 返回结果

    • 所有循环结束后,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)

  1. 循环结构:算法使用了两层嵌套的 for 循环。
    • 外层循环从 i = 0 运行到 n-1,执行 N 次。
    • 内层循环从 j = 0 运行到 i-1。其执行次数依赖于 i
  2. 计算依据
    • 总的操作次数(主要是比较和Math.max)大致等于 0 + 1 + 2 + ... + (N-1)
    • 这是一个等差数列求和,结果为 N * (N - 1) / 2
    • 因此,总的操作次数与 N^2 成正比。

综合分析
算法的时间复杂度为 O(N^2)

空间复杂度:O(N)

  1. 主要存储开销:算法创建了一个名为 dp 的整型数组来存储动态规划的所有中间状态。
  2. 空间大小:该数组的长度与输入数组 nums 的长度 N 相同。
  3. 其他变量n, ans, i, j 等变量都只占用常数级别的空间,即 O(1)。
  4. 迭代实现:与记忆化搜索不同,这种迭代方法没有递归调用栈的开销。

综合分析
算法所需的额外空间主要由 dp 数组决定。因此,其空间复杂度为 O(N)

参考灵神

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xumistore

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值