47. 全排列 II

47. 全排列 II

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

提示:

  • 1 <= nums.length <= 8
  • -10 <= nums[i] <= 10

思路

这道题目和46.全排列的区别在于给定一个可包含重复数字的序列,要返回所有不重复的全排列。

这里又涉及到去重了。

40.组合总和II90.子集II 我们分别详细讲解了组合问题和子集问题如何去重。

那么排列问题其实也是一样的套路。

还要强调的是去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。当然,如果题目要求不能改变原数组,那么就应该使用set辅助去重,如:491. 递增子序列

我以示例中的 [1,1,2]为例 (为了方便举例,已经排序)抽象为一棵树,去重过程如图:注意看绿色部分,说明了回溯过程中的used切片元素变化

在这里插入图片描述

图中我们对同一树层,前一位(也就是nums[i-1])如果使用过要选nums[i]了,那么就进行去重,因为同一树层遍历到第i个元素了,但是used[i-1]却是0,说明是回溯回来的时候将used[i-1]置为0了,不然i-1i之前,肯定会先用i-1,从而used[i-1]1的。另一方面,看上图左下角蓝色字部分,同树枝上used[i]used[i-1]相等,而used[i-1]选了,仍可以选used[i],因为是题目所给数组就有两个相同元素,排列中就是需要他们两个都存在的,即同树枝可以选相同数值的元素,因为是所给数组中他们的值就相同而已。

具体而言,对照上图来看,如果将第二个1看成1',可选集合则是[1,1',2],看成第0层。开始递归,第一层选1,第二层选1’,第三层选2,得到[1,1’,2]。回溯回到第二层,横向选2,得到[1,2],然后选1',得到[1,2,1'],现在又要回溯上去了,直到回到第一层,选1’,然后如果继续递归到第二层,选2,会得到[1',2],而实际上以1'开头,递归下去选其他数的组合,都包含在同层前一个树枝,以1开头去选其他数的情况中了,所以是同层需要去重直接以1'开头的情况的。

一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。

46.全排列 中已经详细讲解了排列问题的写法,在40.组合总和II90.子集II 中详细讲解了去重的写法,所以这次我就不用回溯三部曲分析了,直接给出代码,如下:

func permuteUnique(nums []int) [][]int {
    if len(nums) == 0 {
        return nil
    }
    res := make([][]int,0)
    path := make([]int,0)
    used := make([]bool,len(nums))
    // 涉及去重,所以先排序
    sort.Ints(nums)
    backtracking(nums,&res,&path,used)
    return res
}

func backtracking(nums []int,res *[][]int,path *[]int,used []bool) {
    if len(*path) == len(nums) {
        *res = append(*res,append([]int(nil),*path...))
        return
    }

    for i := 0;i < len(nums);i++ {
   		 // used[i - 1] == true,说明同一树枝nums[i - 1]使用过
        // used[i - 1] == false,说明同一树层nums[i - 1]使用过
        // 同树层相同元素不重复选,去重,注意是used[i - 1]
        if i > 0 && nums[i] == nums[i - 1] && !used[i - 1] {
            continue
        }
        // 同树枝去重,因为每往里递归一层都是从下标0开始取数,nums数组中取过的数字在同一树枝不能重复取
        if used[i] {
            continue
        }
        *path  = append(*path,nums[i])
        used[i] = true
        backtracking(nums,res,path,used)
        *path = (*path)[0:len(*path) - 1]
        used[i] = false
    }
}

在这里插入图片描述

时间复杂度: 最差情况所有元素都是唯一的。复杂度和全排列1都是 O ( n ! ∗ n ) O(n! * n) O(n!n),因为对于 n n n 个元素一共有 n ! n! n! 种排列方案。而对于每一个答案,我们需要 O ( n ) O(n) O(n)去复制单个切片结果,放到最终的 r e s res res数组

空间复杂度: O ( n ) O(n) O(n) 回溯树的深度取决于我们有多少个元素,以及使用了一个标记数组。

拓展

大家发现,去重最为关键的代码为:

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

如果改成 used[i - 1]等价【used[i - 1] == true】, 也是正确的!,去重代码如下:

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

这是为什么呢,就是上面我刚说的,如果要对树层中前一位去重,就用!used[i - 1]等价【used[i - 1] == false】 ,如果要对树枝前一位去重用used[i - 1]

对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!

这么说是不是有点抽象?

来来来,我就用输入: [1,1,1] 来举一个例子。

树层上去重!used[i - 1] ,的树形结构如下:

在这里插入图片描述

树枝上去重used[i - 1]的树型结构如下:

在这里插入图片描述

大家应该很清晰的看到,树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。

总结

这道题其实还是用了我们之前讲过的去重思路,但有意思的是,去重的代码中,这么写:

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

和这么写:

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

都是可以的,这也是很多同学做这道题目困惑的地方,知道!used[i - 1] 也行而used[i - 1] 也行,但是就想不明白为啥。

所以我通过举[1,1,1]的例子,把这两个去重的逻辑分别抽象成树形结构,大家可以一目了然:为什么两种写法都可以以及哪一种效率更高!

这里可能大家又有疑惑,既然 !used[i - 1] 也行而used[i - 1] 也行,那为什么还要写这个条件呢?

直接这样写 不就完事了?

if i > 0 && nums[i] == nums[i - 1]) {
    continue
}

其实并不行,一定要加上 !used[i - 1] 或者used[i - 1] ,因为 used[i - 1] 要一直是 true 或者一直是false 才可以,而不是 一会是true 一会又是false。 所以这个条件要写上。

是不是豁然开朗了!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值