问题描述
- Given a collection of numbers that might contain duplicates, return all possible unique permutations.
- Example :
Input: [1,1,2]
Output:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
问题分析
- 该题是 LeetCode 46. Permutations 的进阶,和 剑指offer-字符串的排列 类似,该题给定的数组含有重复元素,求出所有可能的排列,但排列不能重复。
- 首先,可以用 46那种方法,找出所有的全排列,最终用set去重,可是这样增加了太多的冗余递归,所以必须采用剪枝的策略
- 可以依然采用交换的思想:
只不过当前做选择时,不要做出重复选择即可。可以用set来保存当前做出的决策,只有不重复时,才能继续dfs,剪枝的思想。比如[1,1,2],前序步骤已经选择了2,当前可以做的选择有1和1,为了避免重复,我们只选一次1即可。 - 采用used数组的思想:只不过需要先对数组进行排序
剪枝的策略为:
- 如果当前已经使用,不再走这条分支
- 如果当前数字为数组第一个元素,可以走
- 如果当前数字不等于上一数字,可以走
- 如果上一数字已被使用,可以走(因为对于排序后的1112 而言,若形成路径1112的话,一定是先按照数组的索引那样从前到后依次选择1),如果上一数字已经被使用,那么在当前层便不会使用上一个数字,所以,上一个数字为多少对当前不会产生影响。我们要求的是同一层之间的不重复决策。
经验教训
- 如何去重及剪枝
代码实现
- 交换 + set
public List<List<Integer>> permuteUnique(int[] nums) {
if (nums == null || nums.length == 0) {
return new ArrayList<>();
}
List<List<Integer>> res = new ArrayList<>();
findPermutations(nums, 0, res);
return res;
}
public void findPermutations(int[] nums, int i, List<List<Integer>> res) {
if ( i == nums.length) {
ArrayList<Integer> list = new ArrayList<>();
for (int k = 0; k < nums.length; ++k) {
list.add(nums[k]);
}
res.add(list);
return;
}
HashSet<Integer> set = new HashSet<>();
for (int j = i; j < nums.length; ++j) {
if (! set.contains(nums[j])) {
set.add(nums[j]);
swap(nums, i, j);
//继续 i+1 ~ len 的决策
findPermutations(nums, i + 1, res);
//利用回溯,复原数组。
swap(nums, i ,j);
}
}
return;
}
public void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
- used
public List<List<Integer>> permuteUnique(int[] nums) {
if (nums == null || nums.length == 0) {
return new ArrayList<>();
}
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
findPermutations(nums, 0, used, new ArrayList<Integer>(), res);
return res;
}
public void findPermutations(int[] nums, int i, boolean[] used, ArrayList<Integer> path, List<List<Integer>> res) {
if ( i == nums.length) {
ArrayList<Integer> newPath = new ArrayList<>(path);
res.add(newPath);
return;
}
for (int j = 0; j < nums.length; j++) {
if (used[j]) {
continue;
}
if (j == 0 || (nums[j] != nums[j - 1] || used[j - 1])) {
//如果是第一个或者当前数字与上一数字不同或者上一数字在前续已构造路径中已经使用过了
/*
if (j > 0 && nums[j] == nums[j - 1] && !used[j - 1]) {
continue;
}
*/
used[j] = true;
path.add(nums[j]);
findPermutations(nums, i + 1, used, path, res);
used[j] = false;
path.remove(path.size() - 1);
}
}
return;
}