LeetCode 78. 子集

题目描述

给定一个整数数组 nums,数组中的元素互不相同,返回该数组所有可能的子集。

答案不能包含重复的子集,返回顺序可以任意。

例如:

复制代码
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

初始思路

这道题可以抽象成一个很直接的选择问题:

复制代码
每个元素只有两种状态:选,或者不选。

所以用 DFS 递归枚举每个位置的两种选择即可。

解题思路

定义递归函数:

复制代码
dfs(nums, i, path, ans)

含义是:当前正在考虑下标 i 对应的元素,path 记录目前已经选择的元素。

对于 nums[i],有两个分支:

复制代码
1. 选择 nums[i],把它加入 path,然后递归处理 i + 1
2. 不选择 nums[i],撤销刚才的选择,然后递归处理 i + 1

i == nums.length 时,说明所有元素都已经决定完了,此时 path 就代表一个完整子集,需要拷贝一份加入答案。

这里一定要拷贝 path,不能直接加入原对象,因为 path 后续还会继续被回溯修改。

代码实现

复制代码
class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> ans = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        dfs(nums, 0, path, ans);
        return ans;
    }

    public void dfs(int[] nums, int i, List<Integer> path, List<List<Integer>> ans) {
        if (i == nums.length) {
            ans.add(new ArrayList<>(path));
            return;
        }

        path.add(nums[i]);
        dfs(nums, i + 1, path, ans);

        path.remove(path.size() - 1);
        dfs(nums, i + 1, path, ans);
    }
}

为什么这样写

假设 nums = [1, 2, 3]

递归过程可以理解为:

复制代码
1:选 / 不选
2:选 / 不选
3:选 / 不选

每个元素都有两个选择,一共有 2^n 条选择路径。每条路径走到叶子节点时,就对应一个子集。

比如:

复制代码
选 1,选 2,选 3     -> [1,2,3]
选 1,选 2,不选 3   -> [1,2]
选 1,不选 2,选 3   -> [1,3]
不选 1,不选 2,不选 3 -> []

所以这份代码的本质就是在枚举一棵二叉决策树。

易错点

1. 忘记拷贝 path

错误写法:

复制代码
ans.add(path);

这样加入的是同一个列表对象,后续回溯修改 path 时,答案中的内容也会被影响。

正确写法:

复制代码
ans.add(new ArrayList<>(path));

2. 选完之后忘记撤销

选择当前元素后:

复制代码
path.add(nums[i]);
dfs(nums, i + 1, path, ans);

递归结束必须撤销:

复制代码
path.remove(path.size() - 1);

这样后面的"不选当前元素"分支才不会受到影响。

3. 两个分支都要进入下一层

无论选还是不选当前元素,都说明当前下标 i 已经处理完了,所以下一层都应该递归到 i + 1

复杂度分析

  • 时间复杂度:O(n * 2^n)。一共有 2^n 个子集,每个子集在加入答案时最多需要拷贝 O(n) 个元素。
  • 空间复杂度:O(n)。递归栈和 path 最多存放 n 个元素;如果把返回结果也计入空间,则为 O(n * 2^n)

复盘

这题的关键是把子集问题转换成"选或不选"的二叉决策模型。

我的原始思路是对的:用 DFS 按下标向后递归,每个元素分成选和不选两个分支。

写代码时最需要注意的是三件事:

复制代码
终止时拷贝 path
选完后撤销选择
选和不选都递归到 i + 1

Tips

子集类问题可以先记住这个模板:

复制代码
处理第 i 个元素:
    选它
    不选它

只要每一层都把当前元素的两种状态枚举完整,最终就能得到所有子集。

相关推荐
liliangcsdn4 分钟前
最终留出样本/样本外测试集-holdout解读
算法
爱敲代码的小黄28 分钟前
机器学习入门:从数据、训练到业务决策
算法
数据知道29 分钟前
哈希与密码存储——bcrypt、Argon2、盐值与彩虹表
网络·算法·安全·网络安全·密码学·哈希算法
haon112239 分钟前
树模型在信贷风控怎么用——从决策树到 XGBoost
大数据·人工智能·算法·决策树·机器学习·数据挖掘
wordbaby41 分钟前
BM25 是什么?手把手拆解搜索引擎的核心算法
人工智能·算法
阳明山水1 小时前
从相关到因果:预测科学的因果转向与可识别性挑战
人工智能·深度学习·算法·机器学习·架构
TDengine (老段)1 小时前
TDgpt 使用 — 部署、SQL、算法
大数据·数据库·sql·算法·时序数据库·tdengine·涛思数据
土司大王1 小时前
LeetCode 17 电话号码的字母组合:Java 回溯模板、多叉决策树与复杂度分析
java·leetcode·决策树