LeetCode 47. 全排列 II

122 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第41天,点击查看活动详情.

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]]

解题思路

思路一: 回溯

LeetCode 46. 全排列 - 掘金 (juejin.cn)题目区别在于包含重复数字, 但要求:返回的结果又不能有重复元素。

解决思路: 在遍历的过程中,一边遍历一遍检测,在一定会产生重复结果集的地方剪枝。

如果要比较两个列表是否一样,一个容易想到的办法是对列表分别排序,然后逐个比对。既然要排序,我们就可以 在搜索之前就对候选数组排序,一旦发现某个分支搜索下去可能搜索到重复的元素就停止搜索,这样结果集中不会包含重复列表。

如果涉及考虑重复元素,或者大小比较的情况,对列表排序是一个不错的选择
现在我们知道要排序,重复元素要剪枝,那么该如何剪枝呢?
剪枝的条件即为:和前一个元素值相同(此处隐含这个元素的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!), n 为序列的长度

  • 空间复杂度: O(n)O(n), 需要额外的空间且该空间取决于递归的深度

参考资料

回溯搜索 + 剪枝(Java、Python) - 全排列 II - 力扣(LeetCode)
【HOT 100】47.全排列II Python3 回溯 考虑重复 --> 46.全排列 - 全排列 II - 力扣(LeetCode)