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

Problem: 78. 子集

题目:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

文章目录

整体思路

这段代码同样旨在解决 "子集 (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的每一条路径都代表了子集的构建过程,并且在进入每个递归节点时都收集一次结果,从而不重不漏地生成了所有子集。

完整代码

java 复制代码
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 开始,向后遍历,尝试将每个元素加入 path
        for (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)

参考灵神

相关推荐
啊我不会诶1 小时前
CF每日5题(1500-1600)
c++·学习·算法
Reggie_L2 小时前
Stream流-Java
java·开发语言·windows
黑哒哒的盟友2 小时前
JMeter groovy 编译成.jar 文件
java·jmeter·jar
巴伦是只猫2 小时前
Java 高频算法
java·开发语言·算法
点云SLAM2 小时前
C++中std::string和std::string_view使用详解和示例
开发语言·c++·算法·字符串·string·c++标准库算法·string_view
超浪的晨2 小时前
Java 实现 B/S 架构详解:从基础到实战,彻底掌握浏览器/服务器编程
java·开发语言·后端·学习·个人开发
Littlewith2 小时前
Java进阶3:Java集合框架、ArrayList、LinkedList、HashSet、HashMap和他们的迭代器
java·开发语言·spring boot·spring·java-ee·eclipse·tomcat
88号技师2 小时前
2025年7月Renewable Energy-冬虫夏草优化算法Caterpillar Fungus Optimizer-附Matlab免费代码
开发语言·人工智能·算法·matlab·优化算法
dragoooon343 小时前
优选算法:移动零
c++·学习·算法·学习方法
运维小文3 小时前
初探贪心算法 -- 使用最少纸币组成指定金额
c++·python·算法·贪心算法