一、题目要求
给定一个不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
解集不能包含重复的子集,子集中的元素顺序可以任意。
示例:
输入:nums = [1,2,3]
输出:
[
[],
[1],
[2],
[3],
[1,2],
[1,3],
[2,3],
[1,2,3]
]
二、核心理解:什么是“子集问题”
子集问题的本质是:
对于数组中的每一个元素
都存在两种选择:选 or 不选
如果数组长度是 n,那么最终子集的数量一定是:
2^n
本题可以用两种完全不同但都非常经典的方式解决:
- 迭代枚举(在已有子集的基础上扩展)
- DFS + 回溯(把问题看成一棵决策树)
下面分别展开。
三、方法一:枚举扩展法(迭代构造子集)
思路说明
从空集开始:
res = [[]]
每遍历到一个新数字 x,就做一件事:
- 把当前
res中的每一个子集 - 复制一份
- 在复制的子集后面加上
x - 再统一加入
res
一句话概括:
新元素 = 让所有已有子集“多一条分支”
代码实现
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
// 初始只有一个空集
res.add(new ArrayList<>());
for (int num : nums) {
List<List<Integer>> subset = new ArrayList<>();
// 遍历当前已有的所有子集
for (List<Integer> list : res) {
List<Integer> temp = new ArrayList<>(list);
temp.add(num);
subset.add(temp);
}
// 把新生成的子集加入结果集中
for (List<Integer> l : subset) {
res.add(l);
}
}
return res;
}
}
过程示例(nums = [1,2,3])
初始:
res = [[]]
处理 1:
新增:[1]
res = [], [1]
处理 2:
已有:[], [1]
新增:[2], [1,2]
res = [], [1], [2], [1,2]
处理 3:
新增:[3], [1,3], [2,3], [1,2,3]
特点总结
- 不需要递归
- 思路直观,非常适合理解子集的“扩展本质”
- 本质是一个“滚雪球”的过程
四、方法二:DFS + 回溯(树形搜索)
思路说明
把问题想象成一棵树:
- 每一层,决定是否选某个元素
- 每一个节点,都是一个合法子集
- 走到哪里,都可以把当前路径加入结果
核心思想是:
子集问题 = 从任意起点开始的组合问题
不强调“结束条件”,而是“每一步都是答案”
代码实现
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
dfs(res, nums, 0, new ArrayList<>());
return res;
}
private void dfs(List<List<Integer>> res, int[] nums, int index, List<Integer> subset) {
// 每一个状态都是一个合法子集
res.add(new ArrayList<>(subset));
if (index == nums.length) {
return;
}
for (int i = index; i < nums.length; i++) {
subset.add(nums[i]);
dfs(res, nums, i + 1, subset);
subset.remove(subset.size() - 1); // 回溯
}
}
}
DFS 过程理解
以 [1,2,3] 为例:
[]
├─ [1]
│ ├─ [1,2]
│ │ └─ [1,2,3]
│ └─ [1,3]
├─ [2]
│ └─ [2,3]
└─ [3]
关键点:
index保证不走回头路subset代表当前路径- 回溯是为了“撤销选择,走另一条分支”
为什么要 new ArrayList(subset)
因为 subset 在 DFS 过程中会不断被修改:
- add
- remove
如果直接把 subset 放进 res,最终所有结果都会指向同一个引用,内容会被覆盖。