在刷完「子集 I」之后,很多人第一次遇到「子集 II」都会有一个共同的困惑:
子集不就是 DFS / 回溯吗?
多了重复数字,为什么一下子就不会写了?
这道题的核心难点不在回溯,而在“去重” 。
本文会从「子集 I」出发,对比着讲清楚:
- 子集 I 和子集 II 的本质区别
- 重复子集是怎么产生的
- 为什么一定要排序
if (i > index && nums[i] == nums[i - 1]) continue;到底在干嘛
一、题目描述
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
要求:
- 子集不能重复
- 子集内部顺序不限
- 结果中不能包含重复的子集
示例:
输入:nums = [1,2,2]
输出:
[
[],
[1],
[1,2],
[1,2,2],
[2],
[2,2]
]
二、回顾子集 I(没有重复元素)
先看最基础的「子集 I」思路。
核心思想
- 每个元素:选 / 不选
- 使用 DFS(回溯)枚举所有路径
- 每一层递归,都可以把当前路径加入结果
子集 I 的经典代码结构
res.add(new ArrayList<>(subset));
for (int i = index; i < nums.length; i++) {
subset.add(nums[i]);
dfs(nums, i + 1, res, subset);
subset.remove(subset.size() - 1);
}
这里完全不考虑去重问题,因为题目保证 nums 中没有重复元素。
三、子集 II 的问题到底出在哪?
子集 II 和子集 I 的唯一区别:
nums中可能有重复数字
例如:
nums = [2, 2]
如果你直接套子集 I 的写法,会得到:
[]
[2] ← 第一个 2
[2] ← 第二个 2(重复子集)
[2,2]
问题不是 DFS 写错了,而是:
同一层递归中,选择了值相同但来源不同的元素
四、为什么“排序”是去重的前提?
去重的第一步一定是:
Arrays.sort(nums);
排序后:
[2, 2, 3, 3, 3]
排序的目的不是为了好看,而是为了:
- 让相同元素挨在一起
- 才能在“同一层递归”中判断是否重复选择
不排序,后面的去重判断根本无从谈起。
五、去重的核心:只在“同一层”跳过重复元素
来看这句关键代码:
if (i > index && nums[i] == nums[i - 1]) {
continue;
}
这句话是整道题的灵魂。
怎么理解 i > index?
index:当前递归层的起始位置i > index:说明这是同一层中的后续选择
也就是说:
i == index:本层第一次选这个值,可以i > index && nums[i] == nums[i - 1]:
本层已经选过这个值了,再选会产生重复子集
一句话总结
同一层不能选相同的数,不同层可以
六、结合递归树理解“为什么这样不会漏解”
以 nums = [1,2,2] 为例(已排序):
第一层(index = 0)
- 选 1
- 选 2(第一个 2)
- 跳过第二个 2(同层重复)
第二层(index = 2,在选了第一个 2 的情况下)
- 选第二个 2(允许,不是同一层)
这正好符合题意:
[2]只出现一次[2,2]仍然可以生成
七、完整代码实现
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums); // 1. 排序是去重前提
dfs(nums, 0, res, new ArrayList<>());
return res;
}
private void dfs(int[] nums, int index,
List<List<Integer>> res,
List<Integer> subset) {
// 2. 每到一层,当前路径都是一个合法子集
res.add(new ArrayList<>(subset));
for (int i = index; i < nums.length; i++) {
// 3. 同一层去重
if (i > index && nums[i] == nums[i - 1]) {
continue;
}
subset.add(nums[i]); // 4. 做选择
dfs(nums, i + 1, res, subset); // 5. 进入下一层
subset.remove(subset.size() - 1); // 6. 撤销选择
}
}
}
八、子集 I vs 子集 II 总结对比
| 对比点 | 子集 I | 子集 II |
|---|---|---|
| 是否有重复元素 | 否 | 是 |
| 是否需要排序 | 不需要 | 必须 |
| 是否需要去重 | 不需要 | 需要 |
| 去重位置 | 无 | 同一层 DFS |
| 核心判断 | — | i > index && nums[i] == nums[i - 1] |
九、一句话总结
- 子集问题的本质是 回溯 + DFS
- 子集 II 的难点不在 DFS,而在 控制“同层选择”
- 排序 + 同层去重,是解决所有「重复子集 / 组合」问题的通用套路
如果你能真正理解这一句判断条件,那么 排列 II、组合 II、子集 II 本质上已经被你吃透了。