LeetCode 第 47 题:全排列 II

思路分析

这一题是在 「力扣」第 46 题:“全排列” 的基础上增加了“序列中的元素可重复”这一条件。因此我们还使用“回溯算法”,只不过在构建递归树的过程中需要“剪枝”,以去除重复元素。

“去重复”的思想来源

1、发现困难

这道题我们完全可以按照第 46 题,在结果集中去重,不过在实际编码的时候,我们会发现并不好做,因为可能产生重复的是一个列表对象,如果是简单的数字,直接放在哈希表中去重就好了。

2、在以前做过的问题中寻找经验

我们可以想象一下,**在一个数组中去掉重复元素,除了使用哈希表,更容易想到的是将原始数组排序(升序、降序均可)。

可以确定的是:重复的元素一定不会是数组第 0 号索引位置的元素。因为要相同元素只保留 1 个,为了方便编码,相同元素我们保留第 1 个或者最后 1 个

「力扣」第 15 题:“三数之和” 就利用这样的思路,在遍历到相同元素的第 2 个的时候,将当前循环 continue 掉,这一步也可以认为是“剪枝操作”。放在这一题也是一样的,同样的思路还可以应用于第 39 题、第 40 题、第 78 题、第 90 题。

下面我具体解释一下这个思想应用于本题的过程。在第 46 题中,如果没有重复元素画出的树形图是这样的。

46-1.png

下面的描述基于我为「力扣」第 46 题:“全排列”编写的题解 《回溯算法(Python 代码、Java 代码)

方法:回溯 + 剪枝(剪枝的效果是“去重复”)

下面这段话是解决有重复元素的序列的排列问题的关键:

当数组中有重复元素的时候,可以先将数组排序,排序以后在递归的过程中可以很容易发现重复的元素。当发现重复元素的时候,让这一个分支跳过,以达到“剪枝”的效果,重复的排列就不会出现在结果集中

请看下图,我们把排序以后的数组,就当做它没有重复元素的话,还按照之前的回溯方法,也很容易看出重复的分支,把它剪去即可。

47-1.png

,
47-2.png

,
47-3.png
,
47-4.png

说明

下面这个代码片段:

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

中的 used[i - 1] 加或者不加 ! ,提交到力扣的代码测评系统中都是可以通过的,下面我解释一下原因。

[2, 3, 3, 3] 中的 3 个 3 为例,相同的 3 个 3 有 6 个排列。我们只要保留 1 个就好了。

它们的索引分列是:

  • [1, 2, 3] (数组中的数组表示 3 这个元素在 [2, 3, 3, 3] 这个数组中的索引,在全排列中可能的“排列”,下同)
  • [1, 3, 2]
  • [2, 1, 3]
  • [2, 3, 1]
  • [3, 1, 2]
  • [3, 2, 1]

发现其实又是一个全排列问题。首先联系数组 used[i - 1] 的语义:used[i - 1] == true 表示索引 i 位置的前一个位置元素已经使用过。在 used[i - 1] == true 的时候全部 continue 掉,则只剩下了 used[i - 1] == false 的情况,即当前遍历的元素的之前的元素均未使用过,因此保留了 [3, 2, 1] 这种排列。

反之理解另一种情况。

因此,used[i - 1] 前面加不加感叹号的区别仅在于保留的是相同元素的顺序索引,还是倒序索引;应用于本题,则是相同分支保留的是第 1 个分支还是最后一个分支,它们在结果集中是“等价的”,具体加感叹号对应哪种情况,不加感叹号,对应哪种情况,我个人觉得并不太重要。

以下代码根据我在「力扣」第 46 题:全排列 II 中的题解(上文有给出链接)中的示例代码修改而来,具体修改的地方,在下面代码的注释中有说明。

基于第 46 题,做 2 处修改即可:

1、在开始回溯算法之前,对数组进行一次排序操作,这是上面多次提到的;

2、在进入一个新的分支之前,看一看这个数是不是和之前的数一样,如果这个数和之前的数一样,并且之前的数还未使用过,那接下来如果走这个分支,就会使用到之前那个和当前一样的数,就会发生重复,此时分支和之前的分支一模一样。(这句话特别关键,可以停下来多看两遍,再看一看上面画的那张图)

参考代码:(感谢用户 @carryzz 提供的 C++ 代码)

Python 代码:

class Solution:
    def permuteUnique(self, nums):
        if len(nums) == 0:
            return []

        # 修改 1:首先排序,之后才有可能发现重复分支,升序、倒序均可
        nums.sort()
        # nums.sort(reverse=True)

        used = [False] * len(nums)
        res = []
        self.__dfs(nums, 0, [], used, res)
        return res

    def __dfs(self, nums, index, pre, used, res):
        if index == len(nums):
            res.append(pre.copy())
            return
        for i in range(len(nums)):
            if not used[i]:
                # 修改 2:因为排序以后重复的数一定不会出现在开始,故 i > 0
                # 和之前的数相等,并且之前的数还未使用过,只有出现这种情况,才会出现相同分支
                # 这种情况跳过即可
                if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]:
                    continue
                used[i] = True
                pre.append(nums[i])
                self.__dfs(nums, index + 1, pre, used, res)
                used[i] = False
                pre.pop()

Java 代码:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Solution {

    private List<List<Integer>> res = new ArrayList<>();
    private boolean[] used;

    private void findPermuteUnique(int[] nums, int depth, Stack<Integer> stack) {
        if (depth == nums.length) {
            res.add(new ArrayList<>(stack));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (!used[i]) {
                // 修改 2:因为排序以后重复的数一定不会出现在开始,故 i > 0
                // 和之前的数相等,并且之前的数还未使用过,只有出现这种情况,才会出现相同分支
                // 这种情况跳过即可
                if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                    continue;
                }
                used[i] = true;
                stack.add(nums[i]);
                findPermuteUnique(nums, depth + 1, stack);
                stack.pop();
                used[i] = false;
            }
        }
    }

    public List<List<Integer>> permuteUnique(int[] nums) {
        int len = nums.length;
        if (len == 0) {
            return res;
        }
        // 修改 1:首先排序,之后才有可能发现重复分支
        Arrays.sort(nums);

        // 如果是降序,需要把 nums 变为包装数组类型,输入 Arrays.sort() 方法才生效,并且还要传入一个比较器,搜索之前,再转为基本类型数组,因此不建议降序排序
        // Integer[] numsBoxed = IntStream.of(nums).boxed().collect(Collectors.toList()).toArray(new Integer[0]);
        // Arrays.sort(numsBoxed, Collections.reverseOrder());
        // nums = Arrays.stream(numsBoxed).mapToInt(Integer::valueOf).toArray();

        used = new boolean[len];
        findPermuteUnique(nums, 0, new Stack<>());
        return res;
    }
}

C++ 代码:

// author: carryzz

#include <iostream>
#include <vector>

using namespace std;

class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int> &nums) {
        sort(nums.begin(), nums.end());
        int n = nums.size();
        vector<int> temp;
        vector<vector<int>> res;
        vector<bool> visited(n, false);
        DFS(nums, temp, res, 0, visited);
        return res;
    }

    void DFS(vector<int> &nums, vector<int> &temp, vector<vector<int>> &res, int cursize, vector<bool> &visited) {
        if (cursize == nums.size()) {
            res.push_back(temp);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            if (!visited[i]) {
                if (i > 0 && nums[i] == nums[i - 1] && !visited[i - 1])
                    continue;
                temp.push_back(nums[i]);
                visited[i] = true;
                DFS(nums, temp, res, cursize + 1, visited);

                temp.pop_back();
                visited[i] = false;
            }
        }
    }
};

int main() {
    Solution solution = Solution();
    vector<int> nums;
    nums.push_back(2);
    nums.push_back(3);
    nums.push_back(3);

    vector<vector<int>> res = solution.permuteUnique(nums);

    // 使用索引遍历
    int i, j;
    cout << "Use index : " << endl;
    for (i = 0; i < res.size(); i++) {
        for (j = 0; j < res[0].size(); j++)
            cout << res[i][j] << " ";
        cout << endl;
    }

    // 使用迭代器遍历
    vector<int>::iterator it;
    vector<vector<int>>::iterator iter;
    vector<int> vec_tmp;

    cout << "Use iterator : " << endl;
    for (iter = res.begin(); iter != res.end(); iter++) {
        vec_tmp = *iter;
        for (it = vec_tmp.begin(); it != vec_tmp.end(); it++)
            cout << *it << " ";
        cout << endl;
    }

    return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容

  • 传送门:47. 全排列 II。 给定一个可包含重复数字的序列,返回所有不重复的全排列。示例:输入: [1,1,2]...
    李威威阅读 682评论 0 0
  • 传送门:47. 全排列 II。 给定一个可包含重复数字的序列,返回所有不重复的全排列。示例:输入: [1,1,2]...
    李威威阅读 614评论 0 0
  • LeetCode中与Permutations相关的共有四题:  31. Next Permutation  46....
    chenjieping1995阅读 9,388评论 1 13
  • 线性表中的双指针法是指通过两个指针(游标)来指示线性表中的元素的方法。双指针的使用本身并没有什么神奇之处,但是通过...
    Like_eb56阅读 513评论 0 0
  • 大部分的人都是孤独的,在大部分时间中是处于孤独的状态,有时看似热热闹闹,但静下心来它是孤独的,孤独使人思考。 在与...
    悸柒阅读 100评论 0 0