给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入: nums = [1,2,3]
输出: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入: nums = [0,1]
输出: [[0,1],[1,0]]
示例 3:
输入: nums = [1]
输出: [[1]]
提示:
1 <= nums.length <= 6-10 <= nums[i] <= 10nums中的所有整数 互不相同
🏠 生活案例:排队买奶茶
想象一下,你带了两个朋友(张三、李四、王五)去买奶茶。柜台服务员想知道,你们三个人站成一排,一共有多少种站法?
- 第一步(选择) :谁站第一个?可以是张三、李四或王五。
- 第二步(记录) :如果张三站了第一个,那第二个位置只能从李四和王五里选。
- 第三步(回溯/撤销) :如果刚才选了张三排头,李四排二,王五排三。记录完这种排法后,李四得从第二个位置下来,王五也得下来,换一种排法(比如王五站第二个)。
这个“先尝试、再撤回、换个路子再试”的过程,在算法里就叫“回溯”。
💻 代码实现与生活化注释
这是你图片中代码的完整版本,我加入了一些“生活化”的注释来帮你理解每一行的逻辑:
JavaScript
/**
* @param {number[]} nums - 这一队参与排队的人(比如 [1, 2, 3])
* @return {number[][]} - 所有的排队方案
*/
var permute = function(nums) {
let res = []; // 最终的记事本,用来记录所有成功的排队方案
// 标记位:记录谁已经在队伍里了,防止一个人分身成两个
let isUsed = new Array(nums.length).fill(false);
/**
* 回溯函数:就像是在排队现场指挥
* @param {number[]} path - 当前已经在排队的“小分队”
*/
let backtrack = (path) => {
// 【停止条件】:如果当前小分队的人数和总人数一样,说明这一轮排好了!
if (path.length === nums.length) {
res.push([...path]); // 把这个方案完整地抄在记事本上
return; // 这一轮结束,回到上一步换人
}
// 遍历每一个人,看看谁能站到当前的空位上
for (let i = 0; i < nums.length; i++) {
// 如果这个人已经在队伍里了,就不能重复选,跳过
if (isUsed[i]) continue;
// 【做选择】:让这个人进队
path.push(nums[i]);
isUsed[i] = true; // 在名单上给他打个勾,标记“已在队中”
// 【递归】:继续给下一个位置找人
backtrack(path);
// 【回溯/撤销选择】:这是最关键的一步!
// 等上面的递归结束了,说明这条路走到底了。
// 我们要把最后进队的那个人请出来,腾出位置给下一种可能。
path.pop();
isUsed[i] = false; // 把名单上的勾擦掉,让他可以参与下一种排队组合
}
}
backtrack([]); // 从一个空队伍开始排
return res; // 返回最后记事本上所有的方案
};
🗝️ 核心逻辑拆解
为了让你理解得更透彻,我们可以把这个过程看作一棵决策树:
- 路径 (path) :就是你当前正在尝试的排列方式。
- 选择列表 (nums) :当前位置你可以选哪些人。
- 结束条件:当
path的长度等于nums的长度,说明所有人都有位子了。 - 回溯的魔力:
path.pop()和isUsed[i] = false就像是**“时光倒流”**。它让你在尝试完一种可能性后,能够干净利落地回到上一个状态,去尝试另一种可能性。
如果不做“撤销”的操作,你第一轮排完 [1, 2, 3] 之后,大家就都锁死在位置上了,没法尝试 [1, 3, 2] 这种新组合了。这就是为什么代码里一定要有那两行“还原”代码的原因。