【每日算法】全排列Ⅱ

95 阅读2分钟

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

全排列Ⅱ

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

示例

输入:nums = [1, 1, 2]

输出:

 [
   [1, 1, 2],
   [1, 2, 1],
   [2, 1, 1]
 ]

思路

这道题是第46题 全排列的进阶,数组中可能包含重复的数字,要求我们返回不重复的全排列,我们依然可以使用搜索回溯的方法来做

约束条件

  • 一个数字不能重复的被选

  • 不能产生重复的排列。重复的排列是怎么产生的呢?

    • 例如 [1,1,2],第一次选择第一个 1 ,和第一次选择第二个 1,往后的情况是一模一样的

我们可以使用一个 used 数组来记录使用过的数字,在之后选择的过程中,不再选择使用过的数字了

 if(used[i]) continue

考虑重复

重复就是存在相同的数字,比如:[1, 1', 2],那么结果中的 [1, 1', 2][1', 1, 2] 是一样的,在存储结果的时候,我们只需要选择其中一个。

但是这不是字符串,在保存结果的时候再去判断是否答案里已经保存了这一种情况会比较麻烦,我们可以考虑在生成答案的过程中就将其枝剪(用过的数字就不考虑) ,这样就不会出现重复的答案了。

例如:[2, 1, 1'] 存在之后,当遇到 [2] 遇到 1' ,发现和 1 重复了,就直接枝剪,不考虑之后的所有情况

两个相同数字,可以

  • 两个都选
  • 两个都不选
  • 如果只选一个,那么选哪一个都可以,因为和选择另一个的情况是相同的,所以只有这种情况我们需要枝剪

代码

 var permuteUnique = function(nums) {
   const res = []
   const used = new Array(nums.length)
   nums.sort((a,b) => a-b) // 升序排序
   
   const helper = path => {
     if(path.length === nums.length) { // 元素个数选够了
       res.push(path.slice()) // 将path的拷贝加入结果数组
       return
     }
     
     for(let i=0; i<nums.length; i++) { // 遍历全数组
       if(used[i]) continue // 如果当前数字已经使用过,则跳过
       // 避免产生重复的排列
       if((i-1 >= 0) &&                // 不是第一个数
          (nums[i-1] === nums[i]) &&    // 当前数字和前一个数字相同
          !used[i-1]) {                // 前一个数字没使用过
         continue
       }
       path.push(nums[i]) // 将当前数字塞入路径
       used[i] = true // 记录路径上做过的选择
       helper(path) // 基于上面的结果,递归继续进行选择
       path.pop()  // 回溯:撤销本次路径选择
       used[i] = false // 回溯:设置数字为“未使用”状态
     }
   }
   helper([])
   return res
 };

如果用类似 Set 的方式做去重,也是可行的,但是没有枝剪的话,会做很多无效的搜索。

\