回溯算法篇--全排列II

113 阅读1分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 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;
};