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 个元素:
    选它
    不选它

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

相关推荐
鹿角片ljp5 小时前
LeetCode 46. 全排列|吃透回溯
算法·leetcode·职场和发展
鼎艺创新科技5 小时前
不依赖 UE/Unity:我们如何从零搭建一套国产三维 GIS 渲染引擎
人工智能·算法·unity·游戏引擎·三维电子沙盘
.道阻且长.7 小时前
11.LeetCode算法习题讲解--滑动窗口--将x减到0的最小操作数
算法·leetcode·职场和发展
wenyq78 小时前
LeetCode 2460. Apply Operations to an Array
算法·leetcode
.格子衫.9 小时前
032动态规划之区间DP——算法备赛
算法·动态规划
青 春 记 忆9 小时前
LeetCode 142. 环形链表 II|Python 解法详解
python·leetcode·链表
小欣加油9 小时前
leetcode3069 将元素分配到两个数组中I
数据结构·c++·算法·leetcode·职场和发展
不会代码的小猴9 小时前
7. JSON
开发语言·c++·笔记·qt·算法·json
怪奇云呼军10 小时前
知识库也会注入指令?闪电智能VoiceAgent 如何防住 Prompt Injection
人工智能·python·算法·云计算·音视频
阿里云大数据AI技术10 小时前
基于 EMR Serverless Ray 实现 Qwen 模型批量推理实践
人工智能·算法·agent