78.子集
1.题目描述
给你一个整数数组
nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
2.解题思路
- 如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
- 其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
- 以示例中nums = [1,2,3]为例把求子集抽象为树型结构,如下:
从图中红线部分,可以看出遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合。
3.代码实现
1.确定递归函数的参数和返回值:
- 全局变量数组path为子集收集元素,二维数组res存放子集组合。
- 递归函数定义:从index位置开始寻找数组nums的所有子集,并把子集中放到结果集res中。
//78.子集
List<List<Integer>> res= new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
private void backTracking(int[] nums, int startIndex) {}
2.确定终止条件:
- 从图中可以看出:剩余集合为空的时候,就是叶子节点。那么什么时候剩余集合为空呢?
- 就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了,代码如下:
//递归终止条件
if (startIndex == nums.length) {
return;
}
3.确定单层递归的逻辑:
- 子集收集元素。
- 递归:注意从i+1开始,元素不重复取。
- 回溯。
//单层递归逻辑
for (int i = startIndex; i < nums.length; i++) {
path.add(nums[i]);// 子集收集元素
backTracking(nums, i + 1);// 注意从i+1开始,元素不重复取
path.removeLast(); // 回溯
}
完整代码如下:
class Solution {
//78.子集
List<List<Integer>> res= new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backTracking(nums, 0);
return res;
}
private void backTracking(int[] nums, int startIndex) {
// 收集子集,要放在终止添加的上面,否则会漏掉自己
res.add(new ArrayList<>(path));
// 终止条件可以不加
if (startIndex == nums.length) {
return;
}
for (int i = startIndex; i < nums.length; i++) {
path.add(nums[i]);// 子集收集元素
backTracking(nums, i + 1);// 注意从i+1开始,元素不重复取
path.removeLast(); // 回溯
}
}
}
90.子集II
1.题目描述
- 给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
- 解集不能包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
2.解题思路
- 这道题目和78.子集 (opens new window)区别就是集合里有重复元素了,而且求取的子集要去重。
- 那么关于回溯算法中的去重问题,在40.组合总和II (opens new window)中已经详细讲解过了,和本题是一个套路。
所以理解“树层去重”和“树枝去重”非常重要。
用示例中的[1, 2, 2] 来举例,如图所示: (注意去重需要先对集合排序)
从图中可以看出,同一树层上重复取2 就要过滤掉,同一树枝上就可以重复取2,因为同一树枝上元素的集合才是唯一子集!
3.代码实现
1.确定递归函数的参数和返回值:
- 全局变量数组path为子集收集元素,二维数组res存放子集组合。
- 递归函数定义:从index位置开始寻找数组nums的所有子集,并把子集中放到结果集res中。
List<List<Integer>> res = new ArrayList<>();;
LinkedList<Integer> path = new LinkedList<>();;
//90. 子集 II
private void backTracking(int[] nums, int startIndex) {}
2.确定终止条件:
- 剩余集合为空的时候,就是叶子节点。那么什么时候剩余集合为空呢?
- 就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了,代码如下:
//递归终止条件
if (startIndex == nums.length) {
return;
}
3.确定单层递归的逻辑:
利用二叉搜索树的特性:
相对于78.子集来说只需要多添加树层上了剪枝逻辑就好了。
注意在去重时要对原数组进行排序。
//单层递归逻辑(一定采用中序遍历,左中右)
for (int i = startIndex; i < nums.length; i++) {
// 我们要对同一树层使用过的元素进行跳过
// 注意这里使用i > startIndex
if (i > startIndex && nums[i] == nums[i - 1]) {
continue;
}
path.add(nums[i]);// 子集收集元素
backTracking(nums, i + 1);// 注意从i+1开始,元素不重复取
path.removeLast();// 回溯
}
完整代码如下:
class Solution {
List<List<Integer>> res = new ArrayList<>();;
LinkedList<Integer> path = new LinkedList<>();;
//90. 子集 II
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);// 去重需要排序
backTracking(nums, 0);
return res;
}
private void backTracking(int[] nums, int startIndex) {
res.add(new ArrayList<>(path));
if (startIndex == nums.length) {
return;
}
for (int i = startIndex; i < nums.length; i++) {
// 我们要对同一树层使用过的元素进行跳过
// 注意这里使用i > startIndex
if (i > startIndex && nums[i] == nums[i - 1]) {
continue;
}
path.add(nums[i]);// 子集收集元素
backTracking(nums, i + 1);// 注意从i+1开始,元素不重复取
path.removeLast();// 回溯
}
}
}
491.递增子序列
1.题目描述
- 给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
- 数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
2.解题思路
- 这个递增子序列比较像是取有序的子集。而且本题也要求不能有相同的递增子序列。
- 这又是子集,又是去重,是不是不由自主的想起了刚刚讲过的90.子集II (opens new window)。
- 就是因为太像了,更要注意差别所在,要不就掉坑里了!
- 在90.子集II (opens new window)中我们是通过排序达到去重的目的。
- 而本题求自增子序列,是不能对原数组经行排序的,排完序的数组都是自增子序列了。
- 所以不能使用之前的去重逻辑!
为了有鲜明的对比,我用[4, 7, 6, 7]这个数组来举例,抽象为树形结构如图:
3.代码实现
3.1使用map
1.确定递归函数的参数和返回值:
递归函数定义:本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
//491.递增子序列 使用map
private void backTracking(int[] nums, int startIndex) {}
2.确定终止条件:
- 本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和回溯算法:求子集问题! (opens new window)一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。
- 但本题收集结果有所不同,题目要求递增子序列大小至少为2,所以代码如下:
//递归终止条件
if (path.size() > 1) {
res.add(new ArrayList<>(path));
// 注意这里不要加return,要取树上的节点
}
3.确定单层递归的逻辑:
在图中可以看出,同一父节点下的同层上使用过的元素就不能在使用了。
那么单层搜索代码如下:
//单层递归逻辑
//说明:map是记录本层元素是否重复使用,新的一层map都会重新定义(清空),
//所以要知道map只负责本层!map与path无关,所以不用做回溯处理
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = startIndex; i < nums.length; i++) {
// 不满足递增或者当前层使用了重复元素
if (!path.isEmpty() && nums[i] < path.get(path.size() - 1)
|| map.getOrDefault(nums[i], 0) == 1) {
continue;
}
// 使用过了当前数字就把对应数字的value设置1,没有使用就为0
map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
path.add(nums[i]);// 子集收集元素
backTracking(nums, i + 1);// 注意从i+1开始,元素不重复取
path.removeLast();// 回溯
}
- 对于已经习惯写回溯的同学,看到递归函数上面的
map.put();
,下面却没有对应的回溯之类的操作,应该很不习惯吧!- 这也是需要注意的点,map 是记录本层元素是否重复使用,新的一层map都会重新定义(清空),所以要知道map只负责本层!
完整代码如下:
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums, 0);
return res;
}
//491.递增子序列 方法一:使用map
private void backTracking(int[] nums, int startIndex) {
if (path.size() > 1) {
res.add(new ArrayList<>(path));
// 注意这里不要加return,要取树上的节点
}
//说明:map是记录本层元素是否重复使用,新的一层map都会重新定义(清空),
//所以要知道map只负责本层!map与path无关,所以不用做回溯处理
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = startIndex; i < nums.length; i++) {
// 不满足递增或者当前层使用了重复元素
if (!path.isEmpty() && nums[i] < path.get(path.size() - 1)
|| map.getOrDefault(nums[i], 0) == 1) {
continue;
}
// 使用过了当前数字就把对应数字的value设置1,没有使用就为0
map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
path.add(nums[i]);// 子集收集元素
backTracking(nums, i + 1);// 注意从i+1开始,元素不重复取
path.removeLast();// 回溯
}
}
}
3.2使用数组
完整代码如下:
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums, 0);
return res;
}
//491.递增子序列 方法二:使用int[]
private void backTracking(int[] nums, int startIndex) {
if (path.size() > 1) {
res.add(new ArrayList<>(path));
// 注意这里不要加return,要取树上的节点
}
//这里使用数组来进行去重操作,题目说数值范围[-100, 100]
//说明:isOrUse是记录本层元素是否重复使用,新的一层isOrUse都会重新定义(清空),
//所以要知道isOrUse只负责本层!isOrUse与path无关,所以不用做回溯处理
int[] isOrUse = new int[201];
for (int i = startIndex; i < nums.length; i++) {
// 不满足递增或者当前层使用了重复元素
if (!path.isEmpty() && nums[i] < path.get(path.size() - 1)
|| (isOrUse[nums[i] + 100] == 1)) {
continue;
}
// 记录这个元素在本层用过了,本层后面不能再用了
isOrUse[nums[i] + 100] = 1;
path.add(nums[i]);// 子集收集元素
backTracking(nums, i + 1);// 注意从i+1开始,元素不重复取
path.removeLast();// 回溯
}
}
}