题目描述
给定一个整数数组 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 个元素:
选它
不选它
只要每一层都把当前元素的两种状态枚举完整,最终就能得到所有子集。