「力扣」第 90 题:子集 II(中等)题解
- 链接:子集 II
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1, 2, 2]
输出:
[
[2],
[1],
[1, 2, 2],
[2, 2],
[1, 2],
[]
]
思路:
- 借用「力扣」第 47 题的剪枝思想,先对数组排序,只要搜索起点一样,必然发生重复,此时剪枝即可。
下面讲解为什么要先排序:
- 根据题目意思,子集的每个元素其实是不考虑次序的,因此
[1, 2]
和[2, 1]
在结果集里只能保留一个; - 有重复元素的时候,得到的子集也有重复列表,如何对列表去重呢?排个序以后,再逐个比对,这样做很麻烦,但其实可以在搜索的时候,就排序,然后绕过会产生重复结果的分支。
这里因为不考虑顺序,因此搜索的时候按顺序搜索即可,需要设置状态变量 start
(正是因为按顺序搜索,因此不用设置布尔数组 used
) 和路径变量 path
。
Java 代码:
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
import java.util.Stack;
public class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
int len = nums.length;
if (len == 0) {
return res;
}
// 排序是为了后面剪枝去重
Arrays.sort(nums);
Deque<Integer> path = new ArrayDeque<>();
dfs(nums, 0, len, path, res);
return res;
}
private void dfs(int[] nums, int start, int len, Deque<Integer> path, List<List<Integer>> res) {
res.add(new ArrayList<>(path));
for (int i = start; i < len; i++) {
// 常见的剪枝操作
if (i > start && nums[i] == nums[i - 1]) {
continue;
}
path.addLast(nums[i]);
// 从 i + 1 开始继续枚举,按顺序枚举,所以不会重复
dfs(nums, i + 1, len, path, res);
path.removeLast();
}
}
}
复杂度分析:(理由同「力扣」第 78 题思路 2)
- 时间复杂度: O ( 2 N ) O(2^N) O(2N),整棵树的结点个数最多 2 N 2^N 2N 个。
- 空间复杂度: O ( N × 2 N ) O(N \times 2^N) O(N×2N),保存子集需要长度为 2 N 2^N 2N 的列表,每一个子集的元素最多长度为 N N N。
总结
- 找出所有可能的解,这样的问题一般用回溯算法解决;
- 回溯算法能解决的问题的特点是:解决一个问题有多种办法,每一个办法又分为多个步骤,我们使用遍历的方法得到所有的结果;
- 解决回溯算法首先要画出树形图,以打开思路,进而考虑编码,考虑剪枝条件怎么写;
- 一般画树形图的时候都是一层一层画,这是人的思维,但是编码的时候,用深度优先遍历,这是计算机的思维,好处在《全排列》这个专辑里已经介绍了(借用系统栈、全程可以使用一份状态变量,节约空间)。