题目
!! 题目来源:全排列 - 力扣
分析
对于上面的示例,其实核心思路非常简单暴力:直接写一个三层嵌套的循环去遍历即可。
代码如下:
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;
};
结果如下:
但实际上,我们并不知道输入的 nums 有多长,也就是说,我们无法确定应该嵌套几个循环来遍历 nums。
所以这里要换一个角度进行思考,以 [1, 2, 3] 为例,假设每取一个值,就往下新增一个节点,那么最终应该是一个树的结构,如图所示:
那么这个问题就变成了对树的遍历,同时需要注意的是,当进入下一层的时候,要记住上一层的状态,方便在触边的情况下进行回退,而这样的思路被称为:回溯。
!! 💡 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;
};
结果如下: