DataStructure_Algorithm_HandBook_PreForLeetCode

47. Permutations II

Given a collection of numbers, nums, that might contain duplicates, return all possible unique permutations in any order.

Example 1:

Input: nums = [1,1,2]
Output:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

Example 2:

Input: nums = [1,2,3]
Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

Constraints:

solution

和上一题不一样的点在于,这道题目的 nums 包含了重复的元素,画出递归树,我们也会发现同一层不能选择相同的元素,否则答案就会重复。

var permuteUnique = function(nums) {
     let result = [];
     let used = new Array(nums.length).fill(false);
     dfs(used,[],nums,result);
     return result;
};

function dfs(used,path,nums,result){
    if(path.length === nums.length){
        result.push([...path]);
        return;
    }
    let set = new Set();
    for(let i = 0; i< nums.length; i++){
        if(!used[i] && !set.has(nums[i])){
            path.push(nums[i]);
            set.add(nums[i]);
            used[i] = true;
            dfs(used,path,nums,result);
            path.pop();
            used[i] = false;
        }
    }
}

当然这里的 set 也可以用数组来代替,节省空间,或者我们可以对 nums 进行排序,利用挨在一起的特性来判断,是否选到重复的元素。

但是对于这个数据规模省略这点空间用处不大。

var permuteUnique = function(nums) {
    let result = [];
    nums.sort((a, b) => a - b);
    let used = new Array(nums.length).fill(false);
    dfs(used, [], nums, result);
    return result;
};

function dfs(used, path, nums, result) {
    if (path.length === nums.length) {
        result.push([...path]);
        return;
    }
    for (let i = 0; i < nums.length; i++) {
        if (!used[i]) {
            if (i > 0 && nums[i-1] === nums[i]&& !used[i-1]) continue;
            path.push(nums[i]);
            used[i] = true;
            dfs(used, path, nums, result);
            path.pop();
            used[i] = false;
        }
    }
}

了解 !used[i-1] 这个条件为什么是必要的,确实需要一些深入的思考。我们来分步骤解析这个逻辑,以便更清楚地理解它如何帮助防止重复排列的生成。

考虑重复元素

首先,假设我们有一个排序后的数组,如 [1, 2, 2]。关键在于如何处理这两个 2,使得它们不会在递归生成的排列中出现在相同的位置,造成重复的排列。

为什么需要 !used[i-1]

当我们在数组中遇到连续的相同数字时,比如两个 2,我们的目标是确保在生成排列的过程中,这些相同的数字不会无谓地交换位置,导致生成重复的排列。这就是 if (i > 0 && nums[i-1] === nums[i] && !used[i-1]) continue; 这行代码的作用。

理解 !used[i-1] 的作用

假设我们没有这个条件,仅仅是 i > 0 && nums[i-1] === nums[i]。那么在某些情况下,虽然前一个相同的元素已经被使用并回溯(即已经被放回),我们仍然会跳过当前元素。这样做实际上是错误的,因为前一个元素已经不在当前递归路径中了,我们应该允许当前元素被使用,以生成新的排列。

!used[i-1] 为真(即 used[i-1] 为假),意味着前一个相同的元素没有被使用。这种情况通常出现在我们尝试开始一个新的递归分支,但前一个相同的元素已经被“回溯”并且放回了未使用的状态。如果我们允许当前的元素(nums[i])在这种情况下被使用,那么它将会错误地复制一个已经生成的排列,因为这实际上是在尝试一个已经由前一个元素尝试过的排列路径。

示例

让我们用 [1, 2, 2] 来具体化这个过程:

  1. 选择 1,然后选择第一个 2,递归路径为 [1, 2]
  2. 现在尝试添加第二个 2,路径变为 [1, 2, 2]
  3. 回溯到 [1, 2],然后回溯到 [1]。此时,第一个 2 被标记为未使用。
  4. 如果没有 !used[i-1],我们可能会错误地跳过第二个 2 的使用,尽管此时应该尝试使用它来生成新的排列。

因此,!used[i-1] 确保我们仅在前一个相同元素实际上还处在路径中时,才跳过当前元素。这是一种避免重复而且正确处理数组中连续重复元素的方法。