LeetCode 90. 子集 II —— 在子集 I 的基础上,如何优雅去重?

1 阅读3分钟

在刷完「子集 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 本质上已经被你吃透了。