13.子集||
例题90:给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
需要去重,在树层上去重,可以用used数组或者flag。
Boolean数组先定义boolean[] used;然后初始化used=new boolean(长度);Arrays.fill(used,false)
;
树层去重用used数组的话,注意是当前i的上一个位置为false并且当前i与前一位数相同。
Java定义数组int[] nums=new int[3];必须要初始化
class Solution{
List<List<Integer>> res=new ArrayList<>();
ArrayList<Integer> path=new ArrayList<>();
boolean[] used;
public List<List<Integer>> subsetsWithDup(int[] nums){
used = new boolean[nums.length];
Arrays.fill(used, false);
backtracking(nums,0);
return res;
}
public void backtracking(int[] nums,int startIndex){
res.add(new ArrayList<>(path));
if(startIndex>=nums.length){
return;
}
for(int i=startIndex;i<nums.length;i++){//树层去重
if(i>0 && used[i-1]==false && nums[i]==nums[i-1]){
continue;
}
path.add(nums[i]);
used[i]=true;
backtracking(nums,i+1);
path.remove(path.size()-1);
used[i]=false;
}
}
}
14.递增子序列
例题491:给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
该题的去重稍微有点不同的是,used数组管的是同一层不能有重复的元素
class Solution{
List<List<Integer>> res=new ArrayList<>();
List<Integer> path=new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums){
backtracking(nums,0);
return res;
}
public void backtracking(int[] nums,int startIndex){
int[] used=new int[201];//类似于哈希表存放每个数使用与否,只记录本层元素是否重复使用,新的一层used都会重新定义,所以used只负责本层
if(path.size()>=2 && isDz(path)){
res.add(new ArrayList<>(path));
}
if(startIndex>=nums.length){
return;
}
for(int i=startIndex;i<nums.length;i++){
if(used[nums[i]+100]==1){
continue;
}
path.add(nums[i]);
used[nums[i]+100]=1;//这里加100是因为有负数,而数组下标是从0开始的
backtracking(nums,i+1);
path.remove(path.size()-1);
//used[i]=false;同层去重
}
}
public boolean isDz(List<Integer> path){
for(int i=0;i<path.size()-1;i++){
if(path.get(i)>path.get(i+1)){
return false;
}
}
return true;
}
}
这个题需要注意与以往的子集问题模板写法不同,去重是同层不能有重复的元素,所以在递归之后没有将记录使用过元素的used数组归0.
同时,要用数组来实现哈希表的功能,而数组下标是从1开始,所以要+100让负数从0开始记录。
15.全排列
例题46:给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
全排列和之前的组合、分割、子集略有不同,找到后面的元素还要往前找,不是在数组末尾元素就结束。
排列问题是有序的,{1,2}和{2,1}是两个集合,所以不需要startIndex。但需要used数组标记已选择的元素。如下图所示:
class Solution{
List<List<Integer>> res=new ArrayList<>();
LinkedList<Integer> path=new LinkedList<>();
boolean[] used;
public List<List<Integer>> permute(int[] nums){
used=new boolean[nums.length];
Arrays.fill(used,false);
backtracking(nums,used);
return res;
}
public void backtracking(int[] nums,boolean[] used){
if(path.size()==nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i=0;i<nums.length;i++){
if(used[i]==true){
continue;
}
path.add(nums[i]);
used[i]=true;
backtracking(nums,used);
path.removeLast();
used[i]=false;
}
}
}
16.全排列||
例题47:给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
与上一题不同的是,给的数组中有重复元素,因此全排列要排序去重。
class Solution{
List<List<Integer>> res=new ArrayList<>();
LinkedList<Integer> path=new LinkedList<>();
boolean[] used;
public List<List<Integer>> permuteUnique(int[] nums){
used=new boolean[nums.length];
Arrays.fill(used,false);
Arrays.sort(nums);
//如果for循环中有相同的跳过,树层有相同的也要去重
backtracking(nums,used);
return res;
}
public void backtracking(int[] nums,boolean[] used){
if(path.size()==nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i=0;i<nums.length;i++){
if(i>0 && nums[i]==nums[i-1] && !used[i-1]){
continue;
}
if(used[i]==false){
path.add(nums[i]);
used[i]=true;
backtracking(nums,used);
path.removeLast();
used[i]=false;
}
}
}
}
17.周末总结
1.求子集问题:子集问题是收集所有节点,组合切割问题是收集叶子节点。
2.递增子序列:去掉同一层重复出现的元素,需要用哈希表或数组记录同层元素的出现情况,并对每一层设置used数组。
3.排列问题(无重复数组):每层都是从0开始而不是startIndex,需要used记录path里放了哪些元素。
4.排列问题||(重复数组):需要对for循环中重复元素去重,以及每层从0开始跳过自身。
性能分析
子集问题分析:
- 时间复杂度:
O
(
n
×
2
n
)
O(n × 2^n)
O(n×2n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为
O
(
2
n
)
O(2^n)
O(2n),构造每一组子集都需要填进数组,又有需要
O
(
n
)
O(n)
O(n),最终时间复杂度:
O
(
n
×
2
n
)
O(n × 2^n)
O(n×2n)。
空间复杂度: O ( n ) O(n) O(n),递归深度为n,所以系统栈所用空间为 O ( n ) O(n) O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为 O ( n ) O(n) O(n)。
排列问题分析:
- 时间复杂度: O ( n ! ) O(n!) O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * … 1 = n!。每个叶子节点都会有一个构造全排列填进数组的操作(对应的代码:result.push_back(path)),该操作的复杂度为 O ( n ) O(n) O(n)。所以,最终时间复杂度为:n * n!,简化为 O ( n ! ) O(n!) O(n!)。
- 空间复杂度: O ( n ) O(n) O(n),和子集问题同理。
组合问题分析:
- 时间复杂度: O ( n × 2 n ) O(n × 2^n) O(n×2n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
- 空间复杂度:
O
(
n
)
O(n)
O(n),和子集问题同理。
一般说道回溯算法的复杂度,都说是指数级别的时间复杂度,这也算是一个概括吧!