LeetCode 78. 子集

题目描述

给定一个整数数组 nums,数组中的元素互不相同,返回该数组所有可能的子集。

答案不能包含重复的子集,返回顺序可以任意。

例如:

复制代码
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

初始思路

这道题可以抽象成一个很直接的选择问题:

复制代码
每个元素只有两种状态:选,或者不选。

所以用 DFS 递归枚举每个位置的两种选择即可。

解题思路

定义递归函数:

复制代码
dfs(nums, i, path, ans)

含义是:当前正在考虑下标 i 对应的元素,path 记录目前已经选择的元素。

对于 nums[i],有两个分支:

复制代码
1. 选择 nums[i],把它加入 path,然后递归处理 i + 1
2. 不选择 nums[i],撤销刚才的选择,然后递归处理 i + 1

i == nums.length 时,说明所有元素都已经决定完了,此时 path 就代表一个完整子集,需要拷贝一份加入答案。

这里一定要拷贝 path,不能直接加入原对象,因为 path 后续还会继续被回溯修改。

代码实现

复制代码
class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> ans = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        dfs(nums, 0, path, ans);
        return ans;
    }

    public void dfs(int[] nums, int i, List<Integer> path, List<List<Integer>> ans) {
        if (i == nums.length) {
            ans.add(new ArrayList<>(path));
            return;
        }

        path.add(nums[i]);
        dfs(nums, i + 1, path, ans);

        path.remove(path.size() - 1);
        dfs(nums, i + 1, path, ans);
    }
}

为什么这样写

假设 nums = [1, 2, 3]

递归过程可以理解为:

复制代码
1:选 / 不选
2:选 / 不选
3:选 / 不选

每个元素都有两个选择,一共有 2^n 条选择路径。每条路径走到叶子节点时,就对应一个子集。

比如:

复制代码
选 1,选 2,选 3     -> [1,2,3]
选 1,选 2,不选 3   -> [1,2]
选 1,不选 2,选 3   -> [1,3]
不选 1,不选 2,不选 3 -> []

所以这份代码的本质就是在枚举一棵二叉决策树。

易错点

1. 忘记拷贝 path

错误写法:

复制代码
ans.add(path);

这样加入的是同一个列表对象,后续回溯修改 path 时,答案中的内容也会被影响。

正确写法:

复制代码
ans.add(new ArrayList<>(path));

2. 选完之后忘记撤销

选择当前元素后:

复制代码
path.add(nums[i]);
dfs(nums, i + 1, path, ans);

递归结束必须撤销:

复制代码
path.remove(path.size() - 1);

这样后面的"不选当前元素"分支才不会受到影响。

3. 两个分支都要进入下一层

无论选还是不选当前元素,都说明当前下标 i 已经处理完了,所以下一层都应该递归到 i + 1

复杂度分析

  • 时间复杂度:O(n * 2^n)。一共有 2^n 个子集,每个子集在加入答案时最多需要拷贝 O(n) 个元素。
  • 空间复杂度:O(n)。递归栈和 path 最多存放 n 个元素;如果把返回结果也计入空间,则为 O(n * 2^n)

复盘

这题的关键是把子集问题转换成"选或不选"的二叉决策模型。

我的原始思路是对的:用 DFS 按下标向后递归,每个元素分成选和不选两个分支。

写代码时最需要注意的是三件事:

复制代码
终止时拷贝 path
选完后撤销选择
选和不选都递归到 i + 1

Tips

子集类问题可以先记住这个模板:

复制代码
处理第 i 个元素:
    选它
    不选它

只要每一层都把当前元素的两种状态枚举完整,最终就能得到所有子集。

相关推荐
深圳市快瞳科技有限公司20 小时前
宠物生物特征识别:鼻纹、面部与多模态融合技术的突破
人工智能·算法·计算机视觉·宠物
豆角焖肉20 小时前
冒泡、快排、堆排的实现逻辑与选择思路
java·算法·排序算法·快排·冒泡·堆排
这就是佬们吗20 小时前
回溯算法三板斧---掌握「回溯三问」思考模板快速入门回溯
java·数据库·算法
冻柠檬飞冰走茶20 小时前
PTA基础编程题目集 7-24 约分最简分式(C语言实现)
c语言·开发语言·数据结构·算法
程序喵大人1 天前
【C++进阶】STL容器与迭代器 - 07 迭代器是容器和算法之间的通用游标
开发语言·c++·算法
tkevinjd1 天前
力扣239-滑动窗口最大值
算法·leetcode·职场和发展
wabs6661 天前
关于图论【卡码网105.有向图的完全联通的思考】
数据结构·算法·图论
手写码匠1 天前
828华为云征文|Flexus X实例 + MaaS平台DeepSeek推理服务:从零搭建企业级AI应用全攻略
人工智能·深度学习·算法·aigc
圣保罗的大教堂1 天前
leetcode 628. 三个数的最大乘积 简单
leetcode