【每日算法】全排列

49 阅读3分钟

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

全排列

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

示例

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

输出:[[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]

思路:广度优先

排列是讲究顺序的,不同的顺序会产生不同的排列

模拟搜索过程

  • 以 1 开头的排列,它们是:[1, 2, 3], [1, 3, 2]
  • 以 2 开头的排列,它们是:[2, 1, 3], [2, 3, 1]
  • 以 3 开头的排列,它们是:[3, 1, 2], [3, 2, 1]

为了做到不重不漏

  • 我们需要按顺序枚举每一个位置可能出现的数字
  • 之前已经出现的数字,在接下来要选择的数字中不能再出现
  • 这样的问题,我们可以将其画成一颗的样子

全排列的树形结构

以输入数组 [1, 2, 3] 为例

  • 开始排列为空列表(数组)
  • 第一个位置有三种可能,分别是 1、2、3
     []
   /  |  \
 [1] [2] [3] 
  • 由于第一个位置已经使用了一个数字,那么第二个位置可以使用的数字就只剩两个了

因此,这一层的节点又可以展开两个分支

                []
     /           |          \
    [1]         [2]         [3] 
   /    \      /    \      /    \
 [1,2] [1,3] [2,1] [2,3] [3,1] [3,2]
  • 选出了两个之后,由于一共就只有三个数字供我们选择,因此最后一个位置上的数字就是唯一确定的了

在这些叶子节点中的全部排列,就是 [1,2,3] 的全排列

                      []
       /               |              \
      [1]             [2]             [3] 
    /      \        /      \        /      \
  [1,2]   [1,3]   [2,1]   [2,3]   [3,1]   [3,2]
    |       |       |       |       |       |
 [1,2,3] [1,3,2] [2,1,3] [2,3,1] [3,1,2] [3,2,1]

思路:深度优先

  • 先选第一个数 1
   []
  /
 [1]
  • 再选第二个数 2
     []
   /
  [1]
   |
 [1,2]
  • 最后填上唯一剩下的 3
      []
    /
   [1]
    |
  [1,2]
    |
 [1,2,3]
  • 之后进行回退,撤销对 3 的选择,回到 [1,2] 这个节点
  • 由于这个阶段的只有数字 3 且已经处理过了,因此撤销对 2 的选择,继续回退到 [1]
  • 在这个阶段有两个选择(2 或 3),2 我们已经走过了,接下来选择 3
  • 选择 3 之后,接下来可以选择的又只有 2,此时又可以得到排列 [1,3,2]
         []
       / 
      [1]
    /      \
  [1,2]   [1,3]
    |       |
 [1,2,3] [1,3,2]
  • 以此类推,走完全部的选择,就可以得到最终的全排列

每一个节点标识了求解问题的不同阶段,可以利用变量不同的值来指定,称之为状态

深度优先遍历在回到上一层节点时需要 状态重置

代码实现:深度优先

 var permute = function(nums) {
   const res = [], path = []
   backtracking(nums, nums.length, []) // 调用回溯函数,传入 nums,nums长度,used数组
   return res
   
   function backtracking(n, k, used) {
     if(path.length === k) { // 递归终止条件:已使用长度和原数组长度一致,说明原数组中的元素已经全部使用了
       res.push(Array.from(path))
       return
     }
     for(let i=0; i<k; i++) {
       if(used[i]) continue // 已经使用过了就跳过
       path.push(n[i]) // 插入最近一个没使用过的元素
       used[i] = true // 将这个元素的状态设置为“已使用”
       backtracking(n, k, used) // 递归
       path.pop() // 回溯,将push的元素pop出来
       used[i] = false // 然后标记为“未使用”,继续其他分支
     }
   }
 }

\