一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第15天,点击查看活动详情。
1.题目一
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]]
提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10
思路
回溯法三部曲
- 递归函数的返回值以及参数
- 回溯函数终止条件
- 单层搜索的过程 还是那句话画二叉树图能更清晰了解,此处我就不画图了。
本题与上一篇的全排列有唯一一个不同就是可重复,如果我们单纯使用nums[i] === num[i-1]来判断的话会导致不同层级间的相同元素也无法使用,所以我们借用一个used来帮助判断,代码里面有注释,这里就不多讲了,
递增子序列考虑的情况更多,这也是需要注意的点,use 是记录本层元素是否重复使用,新的一层use都会重新定义(清空),所以要知道use只负责本层!
那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!
当前递归结束后,要将当前的选择撤销,回到选择前的状态,去考察另一个选择,即进入下一轮迭代,尝试另一种切分的可能。
而used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次。
这样才能在解的空间树中,把路走全了,搜出所有的合法部分解。
代码
const permuteUnique = (nums) => {
const res = [];
const used = new Array(nums.length);
nums.sort((a, b) => a - b); // 升序排序
const helper = (path) => {
if (path.length == nums.length) { // 个数选够了
res.push(path.slice()); // path的拷贝 加入解集
return; // 结束当前递归分支
}
for (let i = 0; i < nums.length; i++) { // 枚举出所有的选择
if (used[i]) { // 这个数使用过了,跳过。
continue;
}
if (i - 1 >= 0 && nums[i - 1] == nums[i] && !used[i - 1]) {// !used[i - 1]避免往下递归的时候遇到两个连续的1不选
used[i] = false
continue;
}
path.push(nums[i]); // make a choice
used[i] = true; // 记录路径上做过的选择
helper(path); // explore,基于它继续选,递归
path.pop(); // undo the choice
used[i] = false; // 也要撤销一下对它的记录
}
};
helper([]);
return res;
};