「力扣」第 78 题:“子集”问题描述
给定一组 不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入:nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1, 2, 3],
[1, 3],
[2, 3],
[1, 2],
[]
]
思路 1 :
- 画出树形图:按照“一个数可以选,也可以不选”的思路,画出如下树形图;
![53cdad552f1f663f36009a746a82a5d1.png](https://i-blog.csdnimg.cn/blog_migrate/7e115e53ac3037202b5c561912edb5fa.jpeg)
- 结果出现在哪里?所有符合条件的结果出现在叶子结点中。
- 使用深度优先遍历需要的状态变量:1、当前考虑的是第几个数
index
;2、从根结点到叶子结点的路径path
,不难分析出它是一个栈。
Java 代码:
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public class Solution {
public List<List<Integer>> subsets(int[] nums) {
int len = nums.length;
List<List<Integer>> res = new ArrayList<>();
if (len == 0){
return res;
}
Deque<Integer> path = new ArrayDeque<>();
dfs(nums, len, 0, path, res);
return res;
}
private void dfs(int[] nums, int len, int index, Deque<Integer> path, List<List<Integer>> res) {
if (index == len){
res.add(new ArrayList<>(path));
return;
}
path.addLast(nums[index]);
dfs(nums, len, index + 1, path, res);
path.removeLast();
dfs(nums, len, index + 1, path, res);
}
}
使用示例 [1, 2, 3]
写一段测试代码运行一下,输出结果:
[[], [3], [2], [2, 3], [1], [1, 3], [1, 2], [1, 2, 3]]
符合我们画出的树形图。
复杂度分析:
- 时间复杂度:
,这里
为数组的长度,叶子结点一共有
个,树的高度为
。
- 空间复杂度:
,理由同时间复杂度。保存子集需要长度为
的列表,每一个子集的元素最多长度为
。
思路 2 :
- 画出树形图:按照“按照每一层选出一个数产生分支”的思路,可以画出如下树形图;
![1bb73bdb9dbb8f3c6a212ea01ec52821.png](https://i-blog.csdnimg.cn/blog_migrate/3b43195b09b5a0158d9442bbe481d6b1.jpeg)
- 结果出现在哪里?所有的结点都是符合条件的结果。
- 使用深度优先遍历需要的状态变量:1、从候选数组的哪一个下标开始搜索
start
;2、从根结点到叶子结点的路径path
,这个变量我们多次遇到了。
Java 代码:
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
int len = nums.length;
if (len == 0) {
return res;
}
Deque<Integer> path = new ArrayDeque<>();
dfs(nums, 0, len, path, res);
return res;
}
private void dfs(int[] nums, int begin, int len, Deque<Integer> path, List<List<Integer>> res) {
// 在遍历的过程中,收集符合条件的结果
res.add(new ArrayList<>(path));
for (int i = begin; i < len; i++) {
path.addLast(nums[i]);
dfs(nums, i + 1, len, path, res);
path.removeLast();
}
}
}
复杂度分析:
- 时间复杂度:
,整棵树的结点个数一共是
个。
- 空间复杂度:
,保存子集需要长度为
的列表,每一个子集的元素最多长度为
。
思路 3 :
- 每一个候选数选与不选,这恰恰好是计算机世界里二进制数能够表示的含义,1 表示选择,0 表示不选;
- 因此,我们可以枚举数组长度的二进制数的所有可能十进制值,按照每一个数位的值枚举所有的可能性(这句话没有说得很准确,大家领会意思即可)。
![b03cf77119008ad71e82f6bf82c475e6.png](https://i-blog.csdnimg.cn/blog_migrate/bece09b3989b11c8dcdaaaf3de035555.jpeg)
Java 代码:
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<List<Integer>> subsets(int[] nums) {
int size = nums.length;
int n = 1 << size;
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < n; i++) {
List<Integer> cur = new ArrayList<>();
for (int j = 0; j < size; j++) {
if (((i >> j) & 1) == 1) {
cur.add(nums[j]);
}
}
res.add(cur);
}
return res;
}
}
复杂度分析:
- 时间复杂度:
,这里
为数组的长度,叶子结点一共有
个子集,遍历每一个子集所代表的二进制数有
位。
- 空间复杂度:
,保存子集需要长度为
的列表,每一个子集的元素最多长度为
。
「力扣」第 90 题:“子集 II”问题描述
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1, 2, 2]
输出:
[
[2],
[1],
[1, 2, 2],
[2, 2],
[1, 2],
[]
]
思路:
- 借用「力扣」第 47 题的剪枝思想,先对数组排序,只要搜索起点一样,必然发生重复,此时剪枝即可。
![4d1c593430bbbf21886d0a6d27b49fe4.png](https://i-blog.csdnimg.cn/blog_migrate/f67c8b91be4863f46fe467ba8ffb76c7.jpeg)
复习一下为什么要先排序:
1、根据题目意思,子集的每个元素其实是不考虑次序的,因此[1, 2]
和[2, 1]
在结果集里只能保留一个;
2、有重复元素的时候,得到的子集也有重复列表,如何对列表去重呢?排个序以后,再逐个比对,这样做很麻烦,但其实可以在搜索的时候,就排序,然后绕过会产生重复结果的分支。
- 这里因为是不考虑顺序的,因此搜索的时候按顺序搜索即可,需要设置状态变量
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)
- 时间复杂度:
,整棵树的结点个数最多
个。
- 空间复杂度:
,保存子集需要长度为
的列表,每一个子集的元素最多长度为
。
总结
- 找出所有可能的解,这样的问题一般用回溯算法解决;
- 回溯算法能解决的问题的特点是:解决一个问题有多种办法,每一个办法又分为多个步骤,我们使用遍历的方法得到所有的结果;
- 解决回溯算法首先要画出树形图,以打开思路,进而考虑编码,考虑剪枝条件怎么写;
- 一般画树形图的时候都是一层一层画,这是人的思维,但是编码的时候,用深度优先遍历,这是计算机的思维,好处在《全排列》这个专辑里已经介绍了(借用系统栈、全程可以使用一份状态变量,节约空间)。
(如果有写得不详细的地方和错误的地方,欢迎朋友们指导。)