47. 全排列 II
题目描述
给定一个可包含重复数字的序列 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]]
解题思路
思路一: 回溯
与46. 全排列 题目区别在于包含重复数字, 但要求:返回的结果又不能有重复元素。
解决思路: 在遍历的过程中,一边遍历一遍检测,在一定会产生重复结果集的地方剪枝。
如果要比较两个列表是否一样,一个容易想到的办法是对列表分别排序,然后逐个比对。既然要排序,我们就可以 在搜索之前就对候选数组排序,一旦发现某个分支搜索下去可能搜索到重复的元素就停止搜索,这样结果集中不会包含重复列表。
如果涉及考虑重复元素,或者大小比较的情况,对列表排序是一个不错的选择
现在我们知道要排序,重复元素要剪枝,那么该如何剪枝呢?
剪枝的条件即为:和前一个元素值相同(此处隐含这个元素的index>0),并且前一个元素还没有被使用过
剪枝代码如下:
// 剪枝条件:i > 0 是为了保证 nums[i - 1] 有意义
// 写 !used[i - 1] 是因为 nums[i - 1] 在深度优先遍历的过程中刚刚被撤销选择
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
实现代码如下:
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permuteUnique = function(nums) {
let len = nums.length,
res = [],
// 判断数字是否被选择过
used = {};
// 排序(升序或者降序都可以),排序是剪枝的前提
nums.sort((a, b) => a - b)
dfs(nums, len, 0, [], used, res);
return res;
};
function dfs(nums, len, depth, path, used, res) {
if (depth == len) {
res.push([...path]);
return;
}
for (let i = 0; i < len; i++) {
if (used[i]) {
continue;
}
// 剪枝条件:i > 0 是为了保证 nums[i - 1] 有意义
// 写 !used[i - 1] 是因为 nums[i - 1] 在深度优先遍历的过程中刚刚被撤销选择
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
path.push(nums[i]);
used[i] = true;
// 递归
dfs(nums, len, depth + 1, path, used, res);
used[i] = false;
path.pop();
}
}
-
时间复杂度: O ( n × n ! ) O(n×n!) O(n×n!), n 为序列的长度
-
空间复杂度: O ( n ) O(n) O(n), 需要额外的空间且该空间取决于递归的深度
参考资料
回溯搜索 + 剪枝(Java、Python) - 全排列 II - 力扣(LeetCode)
【HOT 100】47.全排列II Python3 回溯 考虑重复 --> 46.全排列 - 全排列 II - 力扣(LeetCode)