当前位置: 首页 > news >正文

【LeetCode 热题 100】300. 最长递增子序列——(解法一)记忆化搜索

Problem: 300. 最长递增子序列

文章目录

  • 整体思路
  • 完整代码
  • 时空复杂度
    • 时间复杂度:O(N^2)
    • 空间复杂度:O(N)

整体思路

这段代码旨在解决经典的 “最长递增子序列” (Longest Increasing Subsequence, LIS) 问题。问题要求在一个未排序的整数数组中,找到一个子序列,该子序列中的元素严格递增,并且该子序列的长度是所有可能情况中最长的。返回这个最长长度。

该算法采用的是一种 自顶向下(Top-Down)的动态规划 方法,即 记忆化搜索 (Memoization)。它通过递归地寻找以每个元素为结尾的最长递增子序列的长度,最终找出全局的最长长度。

算法的核心逻辑步骤如下:

  1. 状态定义与递归关系

    • 算法的核心是递归函数 dfs(i),其状态定义为:
      dfs(i) = nums 数组中,以 nums[i] 为结尾的最长递增子序列的长度
    • 为了计算 dfs(i),我们需要考虑 nums[i] 可以接在哪个递增子序列的后面。nums[i] 只能接在某个以 nums[j](其中 j < i)为结尾的递增子序列之后,并且必须满足 nums[j] < nums[i]
    • 因此,dfs(i) 的值等于 1 (代表 nums[i] 本身) 加上 “所有满足 j < inums[j] < nums[i]dfs(j) 中的最大值”。
    • 这形成了递归关系:dfs(i) = 1 + max(dfs(j)),其中 0 <= j < inums[j] < nums[i]。如果没有这样的 j,则 max(dfs(j)) 为 0,此时 dfs(i) = 1(子序列只包含 nums[i] 自己)。
  2. 记忆化 (Memoization)

    • 纯粹的递归会导致子问题(如 dfs(j))被多次重复计算。
    • 为了优化,算法使用了一个 memo 数组。memo[i] 用于存储 dfs(i) 的计算结果。
    • dfs(i) 函数的开头,会先检查 memo[i] 是否已经被计算过(在此代码中,通过 > 0 判断)。如果已经计算过,就直接返回存储的结果。
  3. 主函数逻辑

    • lengthOfLIS 函数是主入口。它需要找到全局的最长递增子序列。
    • 全局的最长递增子序列必然会以数组中的某个元素 nums[i] 为结尾。
    • 因此,主函数通过一个 for 循环,遍历所有可能的结尾位置 i(从 0n-1),分别调用 dfs(i) 来计算以 nums[i] 为结尾的LIS长度。
    • 在循环中,用一个 ans 变量来记录并更新所有 dfs(i) 结果中的最大值。
  4. 返回结果

    • 循环结束后,ans 中存储的就是全局的最长递增子序列的长度。

完整代码

class Solution {/*** 计算数组中最长递增子序列的长度。* @param nums 输入的整数数组* @return 最长递增子序列的长度*/public int lengthOfLIS(int[] nums) {int n = nums.length;// ans: 用于存储全局的最长递增子序列长度。int ans = 0;// memo: 记忆化数组。memo[i] 存储 dfs(i) 的结果,即以 nums[i] 结尾的 LIS 长度。// 初始化为 0,因为 LIS 长度至少为 1,0 可以作为“未计算”的标志。int[] memo = new int[n];// 遍历所有可能的结尾位置 ifor (int i = 0; i < n; i++) {// 计算以 nums[i] 结尾的 LIS 长度,并用它来更新全局最大值 ans。ans = Math.max(ans, dfs(i, nums, memo));}return ans;}/*** 记忆化搜索函数,计算以 nums[i] 结尾的最长递增子序列的长度。* @param i 当前子序列的结尾元素的索引* @param nums 原始数组* @param memo 记忆化数组* @return 以 nums[i] 结尾的 LIS 长度*/private int dfs(int i, int[] nums, int[] memo) {// 记忆化检查:如果 memo[i] > 0,说明这个子问题已经计算过,直接返回。if (memo[i] > 0) {return memo[i];}// res: 用于记录在 nums[i] 之前的所有 LIS 长度中的最大值。int res = 0;// 遍历 i 之前的所有元素 jfor (int j = 0; j < i; j++) {// 如果找到一个 nums[j] 小于 nums[i],说明 nums[i] 可以接在以 nums[j] 结尾的 LIS 后面。if (nums[j] < nums[i]) {// 我们取所有可能的“前导”LIS长度中的最大值。res = Math.max(res, dfs(j, nums, memo));}}// 最终结果是:前面最长的 LIS 长度 res,加上 nums[i] 本身 (长度+1)。// 在返回前,将结果存入 memo 数组。return memo[i] = res + 1;}
}

时空复杂度

时间复杂度:O(N^2)

  1. 状态数量:由于记忆化的存在,每个子问题 dfs(i)i0n-1)只会被实际计算一次。总共有 O(N) 个不同的状态。
  2. 每个状态的计算时间:在 dfs(i) 函数内部,主要的开销来自 for 循环,它从 j=0 遍历到 i-1。在最坏的情况下(当 i 接近 n-1 时),这个循环执行大约 O(N) 次。
  3. 主函数循环:外层的 for 循环也执行 N 次。但由于记忆化,dfs(i) 的实际计算只发生一次。
  4. 综合分析
    总时间复杂度 = (状态数量) × (每个状态的计算时间) = O(N) * O(N) = O(N^2)
    或者可以这样看:总共有 Nmemo 条目需要填充,填充每个 memo[i] 需要一个 O(i) 的循环。总计算量是 Σ(i=0 to n-1) O(i) = O(N^2)。

空间复杂度:O(N)

  1. 记忆化数组 memo:创建了一个大小为 N 的数组,占用 O(N) 空间。
  2. 递归调用栈:递归的最大深度可以达到 N(例如,dfs(n-1) 调用 dfs(n-2) …)。因此,递归栈占用的空间是 O(N)

综合分析
算法所需的总空间是 O(N) (memo) + O(N) (stack)。因此,最终的空间复杂度为 O(N)

参考灵神

http://www.dtcms.com/a/347488.html

相关文章:

  • mmap映射物理内存之四内核cache同步
  • 后台管理系统-14-vue3之tag标签页的实现
  • JEI(Journal of Electronic lmaging)SCI四区期刊
  • TypeScript的接口 (Interfaces)讲解
  • Python 版本与 package 版本兼容性检查方法
  • 定时任务——ElasticJob原理
  • ChipCamp探索系列 -- 4. Intel CPU的十八代微架构
  • 【背诵2025】测试
  • 数据结构与算法——树和二叉树
  • 【科研绘图系列】浮游植物的溶解性有机碳与初级生产力的关系
  • 大麦APP抢票
  • 数据结构 之 【AVL树的简介与部分实现】(部分实现只涉及AVL树的插入问题,包括单旋((右单旋、左单旋))、双旋(左右单旋、右左单旋)等操作)
  • 国家自然科学基金(国自然基金)申请技巧详解
  • materials studio中的两种坐标系
  • 基于RISC-V架构的国产MCU在eVTOL领域的应用研究与挑战分析
  • leetcode(同向双指针 滑动窗口)209.长度最小的子数组 713.乘积小于K的子数组 3.无重复字符的最长子串
  • 随机森林1
  • 12 SQL进阶-锁(8.20)
  • 我从零开始学习C语言(14)- 基本类型 PART1
  • FRP 内网穿透全流程部署指南 (Windows/Linux)
  • 不必使用 == 和 ===,更严格的相等性判断 API 来了
  • DFT计算入门(materials studio)---Ni金属表面,几何优化
  • 求职推荐大数据可视化平台招聘系统 Vue+Flask python爬虫 前后端分离
  • 【KO】前端面试四
  • leetcode26:删除有序数组中的重复项Ⅰ(快慢指针解法)
  • 【知识】Elsevier论文接收后的后续流程
  • 【数据结构】跳表的概率模型详解与其 C 代码实现
  • 如何用Redis作为消息队列
  • PyQt6 进阶篇:构建现代化、功能强大的桌面应用
  • Java 线程同步解析