Leetcode 上,有一类题目是用「回溯」法解的。在做这类题之前其实并未了解过什么是回溯,因此如果在实际面试考察过程中遇到还是比较棘手的。
当然,在做了一些此类题之后,发现此类题目的整体思路比较类似,因此简单记录下我个人的思路,算是一个总结。
我们以这道比较「经典」的题:《46. 全排列》 为例。
给定一个不含重复数字的数组
nums,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
比如:[1, 2, 3] 的全排列就是 [1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]。
我们需要先将问题分解,[1, 2, 3] 的全排列,是:
- 第一个元素,以数组中每一个元素起始
1.1. 在这里是
1,2和3 - 第二个元素,则是剩下元素中的每一个元素
2.1. 例如第一个是
1的话,第二个就分别是2和3 - 然后第三个元素是再剩下元素中的每一个元素
3.1. 如果前两个是
1和2,那第三个元素只能是3
抽象成一个简单的流程图如下:
graph TD
Start --> A[第一个元素];
A --> B[剩下的两个元素];
B --> C[剩下的一个元素];
C -- 循环 --> B;
C -- 循环 --> A;
C --> End;
其中第一步(获取第一个元素),我们可以再换个说法:剩下的三个元素。
这样,我们就可以从「剩余元素」的遍历开始着手编码了。
const items = [1, 2, 3];
// 记录结果
const result = [];
// 1. 第一次遍历,获取第一个元素
items.forEach((item, index) => {
// 求剩下的元素数组,先复制下当前数组
const rest = items.slice();
// 在复制的数组中,移除当前遍历到的元素,例如第一个循环,移除了 1,剩余 [2,3]
rest.splice(index, 1);
});
这时候,我们会发现剩下的 [2, 3] 仍需要进行相同的遍历。
因此可以将这个遍历的代码抽成一个函数进行递归:
const items = [1, 2, 3];
function traverse(items) {
// 递归要记得加中止条件
if (items.length === 0) {
return;
}
items.forEach((item, index) => {
const rest = items.slice();
rest.splice(index, 1);
traverse(rest);
});
}
traverse(items);
此时我们的解法已经写的差不多了,但似乎还忘记了什么重要的步骤?
……
保存结果!
要知道,我们是需要完整的排列组合的,也就是说当「剩余元素」这个数组为空的时候,才需要将当前结果放到返回的数组中 -- 而这正好是我们的递归中止条件。
另外,每一步的结果,都需要在执行递归的时候向下传递:
const items = [1, 2, 3];
const result = [];
function traverse(items, current) {
// 递归要记得加中止条件
if (items.length === 0) {
result.push(current);
return;
}
items.forEach((item, index) => {
const rest = items.slice();
rest.splice(index, 1);
traverse(rest, current.concat(item));
});
}
traverse(items, []);
至此,我们的题解已经完成了 -- 而这种解题思路,可以用在一些与此题题目的解法上。
总结下,核心的解题要素:递归 -- 计算当前层级所需的元素和解,并将解带入到之后的递归过程中,在递归中止条件中,处理最终的结果。