LeetCode 90中等回溯 · 去重
子集 II 图解题解
这道题到底在问什么
给定一个可能含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集),结果中不能包含重复的子集。
- nums
- [1,2,2]
- 输出
- [[],[1],[1,2],[1,2,2],[2],[2,2]]
最优解:一步一步想明白
- 3记住这句,下面每一步都在套它。
- 4根节点:空路径也是一个合法子集 → 收集 []。
- 5这一层从下标 0 的 1 往下钻:把它加进 path。
- 6path → [1],进入下一层(从下标 1 继续,避免重复用同一个位置)。
- 7每到一个节点先把当前 path 收进结果:[1]。
- 8这一层从下标 1 的 2 往下钻:把它加进 path。
- 9path → [1,2],进入下一层(从下标 2 继续,避免重复用同一个位置)。
- 10每到一个节点先把当前 path 收进结果:[1,2]。
- 11这一层从下标 2 的 2 往下钻:把它加进 path。
- 12path → [1,2,2],进入下一层(从下标 3 继续,避免重复用同一个位置)。
- 13每到一个节点先把当前 path 收进结果:[1,2,2]。
- 14这条分支走完,撤销 2(path → [1,2]),回到上一层换下一个候选。
- 15这条分支走完,撤销 2(path → [1]),回到上一层换下一个候选。
- 16下标 2 的 2 和同层前一个下标 1 的 2 相同,这一层已经从 2 开过分支了,再选会产生重复子集 → 画 ✗ 剪掉。
- 17这条分支走完,撤销 1(path → []),回到上一层换下一个候选。
- 18这一层从下标 1 的 2 往下钻:把它加进 path。
- 19path → [2],进入下一层(从下标 2 继续,避免重复用同一个位置)。
- 20每到一个节点先把当前 path 收进结果:[2]。
- 21这一层从下标 2 的 2 往下钻:把它加进 path。
- 22path → [2,2],进入下一层(从下标 3 继续,避免重复用同一个位置)。
- 23每到一个节点先把当前 path 收进结果:[2,2]。
- 24这条分支走完,撤销 2(path → [2]),回到上一层换下一个候选。
- 25这条分支走完,撤销 2(path → []),回到上一层换下一个候选。
- 26下标 2 的 2 和同层前一个下标 1 的 2 相同,这一层已经从 2 开过分支了,再选会产生重复子集 → 画 ✗ 剪掉。
⚠️ 容易写错的地方
✗ 错:不排序就去重
✓ 对:必须先 sort 让相同元素相邻
不相邻时同层判断 nums[i]==nums[i-1] 失效
✗ 错:写成 i > 0 判重
✓ 对:是 i > start
i>0 会把「同一条路径上用第二个2」也误剪(那是合法的 [2,2])
✗ 错:用 set 去重子集
✓ 对:靠同层剪枝从源头不产生
set 去重浪费大量重复计算,且要把子集排序当 key
完整代码(Python / C++ / Java)
Python
def subsetsWithDup(nums):
nums.sort() # 排序: 相同元素相邻
res, path = [], []
def dfs(start):
res.append(path[:]) # 每个节点都是一个子集
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i-1]:
continue # 同层去重
path.append(nums[i])
dfs(i + 1)
path.pop() # 回溯撤销
dfs(0)
return resC++
vector<vector<int>> subsetsWithDup(vector<int>& nums){
sort(nums.begin(), nums.end());
vector<vector<int>> res; vector<int> path;
function<void(int)> dfs = [&](int start){
res.push_back(path);
for(int i = start; i < nums.size(); ++i){
if(i > start && nums[i] == nums[i-1]) continue;
path.push_back(nums[i]);
dfs(i + 1);
path.pop_back();
}
};
dfs(0);
return res;
}Java
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums); // 排序: 相同元素相邻
dfs(nums, 0);
return res;
}
private void dfs(int[] nums, int start) {
res.add(new ArrayList<>(path));
for (int i = start; i < nums.length; i++) {
if (i > start && nums[i] == nums[i - 1]) continue; // 同层去重
path.add(nums[i]);
dfs(nums, i + 1);
path.remove(path.size() - 1); // 回溯撤销
}
}
}复杂度
时间
O(n · 2ⁿ)
最多 2ⁿ 个子集,每个拷贝 O(n)
空间
O(n)
递归栈 + path 深度 n(不计结果)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 子集 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
和 LC78 子集(无重复)差在哪?+
只多一行 sort + 同层去重判断 i>start && nums[i]==nums[i-1],其余完全一样。
LC40 组合总和 II 也是这套吗?+
是。同样排序 + 同层去重,只是多一个目标和的剪枝(和 ≤ target 才往下)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 子集 II 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。