LeetCode第47题:全排列II

LeetCode第47题: 全排列II

题目描述:

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]

题目链接:

47. 全排列 II - 力扣(Leetcode)

解题思路:

这道题跟46题类似,用深度优先遍历,暴力搜索nums数组,然后回溯时恢复标记数组的原状。
对于递归函数,其函数出口应当为:在判断递归深度等于nums数组的长度时,将path序列添加到结果集中并返回。
我们可以画出树形结构来帮助理解:

1
2
3
4
if(depth==len){
result.add(new ArrayList(path));
return;
}

函数体中,遍历nums数组的每一个元素,对其进行标记后,将其添加至path序列中:

1
2
3
4
5
6
7
8
9
10
for(int i = 0;i<len;i++){
if(!used[i]){
path.add(nums[i]);
used[i]=true;
dfs(len,nums,depth+1,path,used);
//回溯
path.remove(path.size()-1);
used[i] = false;
}
}

不过这道题要求去除重复的序列,我首先想到的就是利用HashSet去重:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution {
static List<List<Integer>> result;
static HashSet<List<Integer>>set;
public List<List<Integer>> permuteUnique(int[] nums) {
set = new HashSet<>();
result = new ArrayList<>();
//System.out.println(result.toString());
int len = nums.length;
List<Integer> path = new ArrayList<>();
boolean used[] = new boolean[nums.length];
if(nums.length==0){
return result;
}
//System.out.println(result.toString());
dfs(len,nums,0,path,used);
for(List list:set){
//System.out.println(list);
result.add(list);
}
return result;
}
static void dfs(int len,int []nums,int depth,List<Integer> path,boolean []used){
if(depth==len){
if(!set.contains(path))
set.add(new ArrayList(path));
//System.out.println(result.toString());
return;
}
for(int i = 0;i<len;i++){
if(!used[i]){
path.add(nums[i]);
used[i]=true;
dfs(len,nums,depth+1,path,used);
//huishu
path.remove(path.size()-1);
used[i] = false;
}
}
}
}

我们可以看到,这种写法的耗时还是比较高。原因是在递归过程中时需要进行剪枝。
思路是:在遍历的过程中,一边遍历一遍检测,在一定会产生重复结果集的地方剪枝。

如果要比较两个列表是否一样,一个容易想到的办法是对列表分别排序,然后逐个比对。既然要排序,我们就可以 在搜索之前就对候选数组排序,一旦发现某个分支搜索下去可能搜索到重复的元素就停止搜索,这样结果集中不会包含重复列表。

画出树形结构如下:重点想象深度优先遍历在这棵树上执行的过程,哪些地方遍历下去一定会产生重复,这些地方的状态的特点是什么? 对比图中标注 ① 和 ② 的地方。相同点是:这一次搜索的起点和上一次搜索的起点一样。不同点是:

  • 标注 ① 的地方上一次搜索的相同的数刚刚被撤销;
  • 标注 ② 的地方上一次搜索的相同的数刚刚被使用。


产生重复结点的地方,正是图中标注了「剪刀」,且被绿色框框住的地方。

大家也可以把第 2 个 1 加上 ‘ ,即 [1, 1’, 2] 去想象这个搜索的过程。只要遇到起点一样,就有可能产生重复。这里还有一个很细节的地方:

  • 在图中 ② 处,搜索的数也和上一次一样,但是上一次的 1 还在使用中;
  • 在图中 ① 处,搜索的数也和上一次一样,但是上一次的 1 刚刚被撤销,正是因为刚被撤销,下面的搜索中还会使用到,因此会产生重复,剪掉的就应该是这样的分支。
    代码实现方面,在第 46 题的基础上,要加上这样一段代码:
1
2
3
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { 
continue;
}

整体实现代码(java):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution {
static List<List<Integer>> result;
//static HashSet<List<Integer>>set;
public List<List<Integer>> permuteUnique(int[] nums) {
//set = new HashSet<>();
result = new ArrayList<>();
//System.out.println(result.toString());
int len = nums.length;
List<Integer> path = new ArrayList<>();
boolean used[] = new boolean[nums.length];
if(nums.length==0){
return result;
}
//System.out.println(result.toString());
dfs(len,nums,0,path,used);
// for(List list:set){
// //System.out.println(list);
// result.add(list);
// }
return result;
}
static void dfs(int len,int []nums,int depth,List<Integer> path,boolean []used){
if(depth==len){

result.add(new ArrayList(path));
//System.out.println(result.toString());
return;
}
Arrays.sort(nums);
for(int i = 0;i<len;i++){
if(used[i])continue;
if(i>0&&!used[i-1]&&nums[i-1]==nums[i]){
continue;
}
path.add(nums[i]);
used[i]=true;
dfs(len,nums,depth+1,path,used);
//huishu
path.remove(path.size()-1);
used[i] = false;
}
}
}