回溯题目:全排列 II

题目

标题和出处

标题:全排列 II

出处:47. 全排列 II

难度

5 级

题目描述

要求

给定一个可能包含重复数字的数组 nums \texttt{nums} nums,返回其所有可能的不重复全排列。可以按任意顺序返回答案。

示例

示例 1:

输入: nums   =   [1,1,2] \texttt{nums = [1,1,2]} nums = [1,1,2]
输出: [[1,1,2],[1,2,1],[2,1,1]] \texttt{[[1,1,2],[1,2,1],[2,1,1]]} [[1,1,2],[1,2,1],[2,1,1]]

示例 2:

输入: nums   =   [1,2,3] \texttt{nums = [1,2,3]} nums = [1,2,3]
输出: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] \texttt{[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]} [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

数据范围

  • 1 ≤ nums.length ≤ 8 \texttt{1} \le \texttt{nums.length} \le \texttt{8} 1nums.length8
  • -10 ≤ nums[i] ≤ 10 \texttt{-10} \le \texttt{nums[i]} \le \texttt{10} -10nums[i]10

解法

思路和算法

这道题是「全排列」的进阶,要求返回可能包含重复数字的数组 nums \textit{nums} nums 的全排列,结果中的全排列不重复。这道题也可以使用回溯得到全排列,但是需要对结果去重。

n n n 表示数组 nums \textit{nums} nums 的长度。创建列表 temp \textit{temp} temp 用于存储当前排列,生成排列的做法是将数组 nums \textit{nums} nums 中的每个元素按照特定顺序依次加入 temp \textit{temp} temp,当 temp \textit{temp} temp 中的元素个数等于 n n n 时,即可得到一个排列。为了确保结果中的排列不重复,生成排列时需要排除重复的情况。

首先将数组 nums \textit{nums} nums 排序,然后生成全排列。排序之后,相同元素位于数组中的相邻位置,可以利用这一特点去重。

由于数组中的每个元素在排列中只能出现一次,因此需要创建长度为 n n n 的标记数组记录每个元素是否已经加入当前排列。每次将元素加入当前排列时,需要遍历数组,只有尚未加入当前排列的元素才能加入当前排列。

对于去重操作,需要考虑产生重复排列的原因。如果一个数在数组中出现 k k k 次,则对于任意一个排列,将这 k k k 个元素的相对顺序交换之后会得到与原排列重复的排列,因此,为了避免重复排列,需要确保这 k k k 个元素加入排列的顺序是固定的。具体而言,当 i > 0 i > 0 i>0 时,如果 nums [ i ] = nums [ i − 1 ] \textit{nums}[i] = \textit{nums}[i - 1] nums[i]=nums[i1](此时数组 nums \textit{nums} nums 已排序),则需要确保 nums [ i − 1 ] \textit{nums}[i - 1] nums[i1] nums [ i ] \textit{nums}[i] nums[i] 之前加入排列。

根据上述分析,在排序后的数组 nums \textit{nums} nums 中遍历到下标 i i i 时,以下两种情况不应将 nums [ i ] \textit{nums}[i] nums[i] 加入当前排列。

  • 如果 nums [ i ] \textit{nums}[i] nums[i] 已经加入当前排列,则不能多次加入当前排列。

  • 如果当 i > 0 i > 0 i>0 时, nums [ i ] = nums [ i − 1 ] \textit{nums}[i] = \textit{nums}[i - 1] nums[i]=nums[i1] nums [ i − 1 ] \textit{nums}[i - 1] nums[i1] 未加入当前排列,则不能将 nums [ i ] \textit{nums}[i] nums[i] 加入当前排列,否则 nums [ i − 1 ] \textit{nums}[i - 1] nums[i1] 将在 nums [ i ] \textit{nums}[i] nums[i] 之后加入当前排列,导致出现重复排列。

其余情况下,执行如下操作。

  1. nums [ i ] \textit{nums}[i] nums[i] 加入当前排列,并将该元素的状态更新为已经加入当前排列,然后继续回溯。

  2. 将当前排列的末尾元素(即 nums [ i ] \textit{nums}[i] nums[i])移除,并将该元素的状态更新为未加入当前排列,恢复到原始状态。

遍历结束时,即可得到数组 nums \textit{nums} nums 的无重复全排列。

代码

class Solution {
    List<List<Integer>> permutations = new ArrayList<List<Integer>>();
    List<Integer> temp = new ArrayList<Integer>();
    int[] nums;
    int n;
    boolean[] visited;

    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        this.nums = nums;
        this.n = nums.length;
        this.visited = new boolean[n];
        backtrack(0);
        return permutations;
    }

    public void backtrack(int index) {
        if (index == n) {
            permutations.add(new ArrayList<Integer>(temp));
        } else {
            for (int i = 0; i < n; i++) {
                if (visited[i] || (i > 0 && nums[i] == nums[i - 1] && !visited[i - 1])) {
                    continue;
                }
                temp.add(nums[i]);
                visited[i] = true;
                backtrack(index + 1);
                temp.remove(index);
                visited[i] = false;
            }
        }
    }
}

复杂度分析

  • 时间复杂度: O ( n × n ! ) O(n \times n!) O(n×n!),其中 n n n 是数组 nums \textit{nums} nums 的长度。排序需要 O ( n log ⁡ n ) O(n \log n) O(nlogn) 的时间,回溯方法的调用次数是 ∑ i = 1 n A n i = ∑ i = 1 n n ! ( n − i ) ! = O ( n ! ) \sum_{i = 1}^{n} A_n^i = \sum_{i = 1}^{n} \dfrac{n!}{(n - i)!} = O(n!) i=1nAni=i=1n(ni)!n!=O(n!),对于每个排列需要 O ( n ) O(n) O(n) 的时间添加到答案中,因此时间复杂度是 O ( n × n ! ) O(n \times n!) O(n×n!)

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。排序需要 O ( log ⁡ n ) O(\log n) O(logn) 的递归调用栈空间,回溯过程中存储当前排列的列表、标记数组和递归调用栈需要 O ( n ) O(n) O(n) 的空间,因此空间复杂度是 O ( n ) O(n) O(n)。注意返回值不计入空间复杂度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

伟大的车尔尼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值