【Daily Interview】- 19 全排列

757 阅读2分钟

题目

img01
img01

!! 题目来源:全排列 - 力扣

分析

对于上面的示例,其实核心思路非常简单暴力:直接写一个三层嵌套的循环去遍历即可

代码如下:

var permute = function (nums) {
  const result = [];
  let temp = [];
  for (i of nums) {
    for (j of nums) {
      if (i === j) continue;
      for (k of nums) {
        if (i === k || j === k) continue;
        temp = [i, j, k];
      }
      result.push(temp);
    }
  }
  return result;
};

结果如下:

img02
img02

但实际上,我们并不知道输入的 nums 有多长,也就是说,我们无法确定应该嵌套几个循环来遍历 nums。

所以这里要换一个角度进行思考,以 [1, 2, 3] 为例,假设每取一个值,就往下新增一个节点,那么最终应该是一个树的结构,如图所示:

img03
img03

那么这个问题就变成了对树的遍历,同时需要注意的是,当进入下一层的时候,要记住上一层的状态,方便在触边的情况下进行回退,而这样的思路被称为:回溯。

!! 💡 tip:回溯本质上就是暴力的遍历每一种结果,效率不能算高,不过应用场景非常广,诸如排列组合,数独迷宫,皇后问题等,都能用回溯算法进行解决,感兴趣的读者可以看看这里

这里设用于回溯的方法叫 backtrace,对于它而言,需要注意的关键点如下:

  • 需要哪些参数?
    • 需要的参数至少有最终的结果 result,用于缓存状态的 cache,以及目标 nums
  • 边界条件是什么?
    • 边界条件就是遍历到树的底层,显然当 cache 的大小等于 nums 的大小的时候,说明已经触底了
  • 如何进入下一层,又如何回到上一层?
    • 这里可以利用栈来进行缓存,先将当前节点入栈,随后递归进入下一层,结束递归后将上层节点出栈即可

结合上面的思路,我们可以书写出下面的代码:

const backtrace = (result, cache, nums) => {
  if (cache.length === nums.length) {
    return result.push([...cache]);
  }
  for (let i = 0; i < nums.length; i++) {
    if (cache.includes(nums[i])) continue;
    cache.push(nums[i]);
    backtrace(result, cache, nums);
    cache.pop();
  }
};

var permute = function (nums) {
  const result = [];
  backtrace(result, [], nums);
  return result;
};

结果如下:

img04
img04