这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战
前言
关于 LeetCode 数组类型题目的相关解法,可见LeetCode 数组类型题目做前必看,分类别解法总结了题目,可以用来单项提高。觉得有帮助的话,记得多多点赞关注哦,感谢!
题目描述
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
示例 2:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
链接:leetcode-cn.com/problems/pe…
题解
- 构建Set. 本题目和LeetCode 46 Permutations的区别在于, 之前的题目数组中不含重复数字, 而本题目中含有重复数字, 如果还按照46题的解法, 就会出现重复的全排列, 因此本题的关键是如何在上一题的解法之上, 增加去重的操作. 那么, 首先可以想到的就是采用 JS 中的数据结构 Set, Set 集合类型保证了里面的元素是唯一的, 但是要注意的点是, 如果是数组, Set 并不能去重, 它会以为 [1,1,2] 和 [1,1,2] 这两个数组是不同的元素, 因此我们可以先转换为字符串插入, 后面再解构成数组. 具体代码如下.
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permuteUnique = function(nums) {
const n = nums.length
let used = new Array(n).fill(0)
let ansSet = new Set()
const dfs = (nums, d, n, used, curr, ansSet) => {
if (d === n) {
// 化为字符串添加进去
ansSet.add(curr.join(','))
return
}
for (let i = 0; i < n; ++i) {
if (used[i]) continue
used[i] = 1
curr.push(nums[i])
dfs(nums, d + 1, n, used, curr, ansSet)
used[i] = 0
curr.pop()
}
}
dfs(nums, 0, n, used, [], ansSet)
// 将 Set 转换为 Array
let ans = Array.from(ansSet)
.map(item => {
let tmp = item.split(',')
return tmp.map(Number)
})
return ans
};
- 剪枝操作. 什么意思呢? 就是我们在递归的过程中直接跳过会重复的排列. 怎么做到呢? 分为两步, 第一步, 将数组排序, 这样相同的元素在数组中就是相邻的; 第二步, 也是最重要的一步, 当遍历到相同元素时, 如果它不是第一位且它前面的相同的元素没有被使用, 那么就跳过这次递归. 为什么是这个条件呢? 比如 [1, 1, 2] 前两个组合为 [1, 1, 2], [1, 2, 1], 下一次递归时, curr 数组为 [], 递归到了第二个 1 上, 也就是下标为 1 的位置, 如果 curr 再以这个 1 为开始, 那么后面遍历到的肯定是重复的; 还有一个条件是前面的元素没有被使用, 这是因为当前面的 1 未使用时, 说明下一个 1 一定是可以跳过的, 前面的 1 已经包括了以 1 开头的所有组合. 代码见下方.
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permuteUnique = function(nums) {
const n = nums.length
let path = []
let res = []
nums = nums.sort((a,b) => a - b)
let use = new Array(n).fill(0)
const dfs = (d) => {
if (d === n) {
res.push([...path])
return
}
for (let i = 0; i < n; i++) {
if (use[i]) continue
if (i > 0 && nums[i] === nums[i-1] && !use[i-1]) continue
path.push(nums[i])
use[i] = 1
dfs(d+1)
use[i] = 0
path.pop()
}
}
dfs(0)
return res
};