LeetCode 78. 子集(回溯法)

0 阅读2分钟

image.png

一、核心思路

子集问题属于回溯组合类题型,数组元素互不相同。 关键两点:

  1. 每走到递归的一层,当前路径path就是一个合法子集,直接加入结果集(和全排列只收集叶子节点不同);
  2. 使用start下标控制遍历起点,下一轮只能选当前下标后面的元素,防止出现[1,2]、[2,1]这种重复子集。

二、完整 Java 代码

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        backtrack(nums, 0);
        return res;
    }

    private void backtrack(int[] nums, int start) {
        // 步骤1:先把当前path存入结果,一定要新建集合拷贝
        res.add(new ArrayList<>(path));
        // 步骤2:从start开始向后遍历
        for(int i = start; i < nums.length; i++){
            // 步骤3:选择当前元素
            path.add(nums[i]);
            // 步骤4:递归,下一轮只能选i之后的元素 i+1
            backtrack(nums, i + 1);
            // 步骤5:回溯,撤销选择
            path.remove(path.size() - 1);
        }
    }
}

三、实例演示(nums = [1,2,3],表格模拟)

递归 startpath是否加入 res循环 i操作
0[]✅ 加入,res: [[]]i=0add 1 → path=[1],递归 start=1
1[1]✅ 加入,res: [[],[1]]i=1add 2 → path=[1,2],递归 start=2
2[1,2]✅ 加入,res: [[],[1],[1,2]]i=2add3 → path=[1,2,3],递归 start=3
3[1,2,3]✅ 加入,res: [[],[1],[1,2],[1,2,3]]i>=3 循环结束return,撤销 3 → path=[1,2]
回到 start=2[1,2]-i 循环结束return,撤销 2 → path=[1]
回到 start=1[1]-i=2add3 → path=[1,3],递归 start=3
3[1,3]✅ 加入i>=3 循环结束return,撤销 3 → path=[1]
回到 start=1[1]-i 循环结束return,撤销 1 → path=[]
回到 start=0[]-i=1add2 → path=[2],递归 start=2
2[2]✅ 加入i=2add3 → path=[2,3],递归 start=3
3[2,3]✅ 加入循环结束return,撤销 3 → path=[2]
回到 start=2[2]-i 循环结束return,撤销 2 → path=[]
回到 start=0[]-i=2add3 → path=[3],递归 start=3
3[3]✅ 加入循环结束return,撤销 3 → path=[]

循环全部结束,最终结果:[[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]

四、复杂度分析

  • 时间复杂度:(O(n* 2^n)),总共有(2^n)个子集,保存每个子集最多拷贝 n 个元素
  • 空间复杂度:(O(n)),递归栈最深为 n,path 最多存放 n 个元素

五、易错点总结

  1. res.add(new ArrayList<>(path)),必须 new 新集合,否则 path 后续修改会覆盖 res 里存好的数据;
  2. 递归传参是i+1,不是start+1,保证只能向后选,避免重复组合;
  3. 子集是每层都收集,不是等循环结束再收集,空集自动被第一层收录,不用手动添加;
  4. 数组元素互不相同,不需要去重。

六、感悟

子集是回溯组合问题的基础模板,和全排列区分开:

  • 子集:start 控制往后选,每层收集 path,不回头;
  • 全排列:used 标记是否使用,每次从头遍历,只在 path 长度等于 nums 长度时收集。