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

【LeetCode 热题 100】78. 子集——(解法二)回溯+选哪个

Problem: 78. 子集
题目:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

文章目录

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

整体思路

这段代码同样旨在解决 “子集 (Subsets)” 问题,即找出给定数组的所有可能子集。与上一个“选或不选”的实现方式不同,此版本的回溯法思路可以理解为 构建组合 的模型。

算法的整体思路可以分解为以下步骤:

  1. 决策模型:构建组合

    • 该算法将生成子集的过程,看作是从原数组 nums 中挑选 0 个、1 个、2 个……直到 n 个元素来组成不同长度的子集。
    • 它定义了一个 dfs(i, ...) 函数,其核心任务是:从 nums 数组的第 i 个元素开始,向后选择元素,加入到当前正在构建的子集 path 中。
  2. 递归与回溯的核心逻辑

    • dfs 函数被调用时,首先要做的一件事是 将当前 path 的状态加入结果集
      • ans.add(new ArrayList<>(path)):这意味着,无论是空集(第一次调用时)、只含一个元素的子集、还是包含多个元素的子集,只要进入 dfs 函数,path 的当前状态就是一个合法的子集。这与“选或不选”模型只在叶子节点收集结果的方式不同。
    • 接下来,通过一个 for 循环来做选择。这个循环从当前位置 i 开始,而不是从 0 开始。
      • for (int j = i; j < nums.length; j++): 这个循环的作用是,在当前层级的决策中,我们可以选择 nums[i], nums[i+1], nums[i+2] 等等中的任意一个,作为下一个要加入 path 的元素。
    • 选择 (Choose)path.add(nums[j])。将选中的元素 nums[j] 加入路径。
    • 探索 (Explore)dfs(j + 1, nums, ...)。做出选择后,递归地进入下一层。注意,下一层的搜索起点是 j + 1,而不是 i + 1。这确保了我们不会重复选择同一个元素,并且生成的子集中的元素顺序是按照原数组的顺序来的(例如,不会出现 [3, 1] 这样的组合,只会是 [1, 3])。
    • 撤销选择 (Unchoose / Backtrack)path.removeLast()。当对 j + 1 的探索返回后,撤销刚才的选择,将 nums[j]path 中移除。这样,在下一次 for 循环中,就可以尝试选择 nums[j+1] 作为当前层级的选择了。
  3. 递归的启动

    • 初始调用是 dfs(0, ...),这意味着第一次我们可以在 nums[0...n-1] 中任意选择一个元素作为子集的第一个元素。

通过这种方式,DFS的每一条路径都代表了子集的构建过程,并且在进入每个递归节点时都收集一次结果,从而不重不漏地生成了所有子集。

完整代码

class Solution {/*** 计算给定数组的所有子集(幂集)。* (组合模型的回溯实现)* @param nums 不含重复元素的整数数组* @return 包含所有子集的列表*/public List<List<Integer>> subsets(int[] nums) {// ans: 最终的结果列表,用于存储所有子集List<List<Integer>> ans = new ArrayList<>();// path: 用于存储当前正在构建的单个子集路径List<Integer> path = new ArrayList<>();// 从索引 0 开始进行深度优先搜索dfs(0, nums, ans, path);return ans;}/*** 深度优先搜索(回溯)辅助函数。* @param i 当前选择的起点索引。表示在这一层,我们可以从 nums[i] 开始向后选择元素。* @param nums 原始输入数组* @param ans 结果列表* @param path 当前构建的子集*/private void dfs(int i, int[] nums, List<List<Integer>> ans, List<Integer> path) {// 关键步骤:在每个递归节点,都将当前 path 的一个深拷贝加入结果集。// 这代表了以当前 path 为前缀的所有子集中的一个。// 第一次调用时,path为空,加入的是空集。ans.add(new ArrayList<>(path));// 从当前起点 i 开始,向后遍历,尝试将每个元素加入 pathfor (int j = i; j < nums.length; j++) {// 选择 (Choose): 将 nums[j] 加入当前子集path.add(nums[j]);// 探索 (Explore): 递归地去构建更长的子集。// 注意:下一层的起点是 j + 1,确保每个元素只用一次,且组合不重复。dfs(j + 1, nums, ans, path);// 撤销选择 (Unchoose / Backtrack): 将刚刚加入的 nums[j] 移除,// 恢复到上一层状态,以便 for 循环可以尝试下一个 j。path.remove(path.size() - 1); // removeLast() is more efficient if path is a LinkedList}}
}

时空复杂度

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

  1. 子集数量:此算法同样会生成 2^N 个子集。
  2. 构建每个子集的成本
    • 该算法的DFS树结构与前一个版本不同,但最终生成的子集数量和总的计算量级是相同的。
    • 在每次 dfs 调用时(总共有 2^N 次调用),都会执行一次 ans.add(new ArrayList<>(path))
    • path 的长度从 0 到 N 不等。所有 path 的总长度加起来,可以证明是 O(N * 2^N)。
    • 因此,所有复制操作的总时间消耗是 O(N * 2^N)

综合分析
尽管实现方式不同,但解决问题的根本计算量没有改变。总时间复杂度仍然是 O(N * 2^N)

空间复杂度:O(N)

  1. 主要存储开销:我们分析的是额外辅助空间,不包括存储最终结果的 ans 列表。
    • List<Integer> path: 用于存储当前路径。path 的最大长度为 N。空间复杂度为 O(N)
    • 递归调用栈dfs 函数的最大递归深度为 N (当构建包含所有 N 个元素的子集时,路径为 0 -> 1 -> ... -> N-1)。因此,递归栈所占用的空间为 O(N)

综合分析
算法所需的额外辅助空间由 path 和递归栈深度共同决定。它们都是 O(N) 级别的。因此,总的辅助空间复杂度为 O(N)

参考灵神

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

相关文章:

  • 第十一章 W55MH32 SMTP示例
  • C# 值类型与引用类型的储存方式_堆栈_
  • Java面试宝典:Spring专题一
  • C语言-函数
  • springboot 3.0 和 2.0 校验用的包不一样
  • 第1章第2章笔记
  • python自动化测试框架,封装方法方式
  • Vivado报错信息[Place 30-574] Poor placement for routing between an IO pin and BUFG
  • 【图像处理基石】如何对遥感图像进行目标检测?
  • 新手向:Pycharm的使用技巧
  • 14. 如何获取用户浏览器内核
  • Qt调试技巧与常见错误解决方法
  • 2025最新Mybatis-plus教程(二)
  • UE中:动态平衡与调度像素流实例方案
  • 【esp32s3】GPIO 寄存器 开发解析
  • 搭建 Android 开发环境JAVA+AS
  • AI赋能DBA:数据库管理与运维的智能化工具全景解析
  • 快手DHPS:国内首个实现基于RDMA 通信的可负载均衡高性能服务架构!
  • AI赋能数据库运维:智能工具如何重塑DBA工作范式
  • PointLLM - ECCV 2024 Best Paper Candidate
  • 深度图像滤波
  • rust嵌入式开发零基础入门教程(六)
  • 10. isaacsim4.2教程-RTX Lidar 传感器
  • centos9 ssh能连接密码不对
  • 用马尔可夫模型进行自动驾驶安全分析
  • 完整的 Redis 5.0 安装、配置及自启动步骤,包含开放端口和密码设置
  • 【网安-小迪】Day5:反弹SHELL不回显带外正反向连接防火墙出入站文件下载
  • 我的第一个开源项目|集成语音感知与云平台的多任务智能楼宇控制系统
  • GaussDB null的用法
  • 第一站之春晖小区——关爱老年人,我们在行动