二十天刷题计划-- 全排列 II

107 阅读1分钟

「这是我参与2022首次更文挑战的第27天,活动详情查看:2022首次更文挑战」。

1.题目

全排列 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


思路

举例子:[1,2,2],选择前两个数,或者第一、三个数,都会得到相同的子集

一般像这种包含重复元素的,而题解答案中又要求去重的,通用套路就是先排序,排序后相同元素相邻可快速去重

若发现没有选择上一个数,还是以上面的例子来说,我已经选择了1,但是没有选择第一个2,而当前数字与上一个数相 同,则可以跳过当前生成的子集。(因为选择前两个数,或者第一、三个数,都会得到相同的子集)

复杂度分析

我们使用一个 used 数组记录使用过的数字,使用过了就不再使用:

if (used[i]) {
    continue;
}

对应上面第二点,如果当前的选项nums[i],与同一层的前一个选项nums[i-1]相同,且nums[i-1]存在,且没有被使用过,则忽略选项nums[i]。

如果nums[i-1]被使用过,它会被第一条修剪掉,不是选项了,即便它和nums[i]重复,nums[i]还是可以选的。

if (i - 1 >= 0 && nums[i - 1] == nums[i] && !used[i - 1]) {

    continue;
    
}

比如[1,1,2],第一次选了第一个1,第二次是可以选第二个1的,虽然它和前一个1相同。 因为前一个1被选过了,它在本轮已经被第一条规则修掉了,所以第二轮中第二个1是可选的。


代码

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]) { // 避免产生重复的排列
                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;
};