46. 全排列#
今天要拆解的这道题,是无数算法题的基础原型。很多同学第一次遇到时都会被绕晕,但只要理解了它的思维模式,你会发现自己打开了新世界的大门!
🧩 从密码锁说起#
小红最近买了一个新型密码锁,发现了一个有趣的规律:
- 密码由3个不同数字组成
- 每个数字都必须使用且只能用一次
- 数字的顺序不同就算不同密码
这不就是典型的全排列问题吗?
问题描述#
题目目标#
给定一个不含重复数字的整数数组 nums ,返回其所有可能的全排列。你可以按任意顺序返回答案。
示例 1#
输入: nums = [1,2,3]
输出: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
说明: 把输入和输出放在一起看,会更容易直接抓住题目要求的变化。
💡 问题的本质#
LeetCode 46题”全排列”的描述是这样的:
给定一个不含重复数字的数组 nums
返回其所有可能的全排列
示例:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]plaintext🤔 这题的关键是什么?#
本质上,我们在寻找:
- 所有可能的元素组合方式
- 通过递归回溯尝试所有选择路径
- 及时剪枝避免重复使用元素
这就是经典的回溯算法问题!
🎬 模拟运行:看看算法是如何工作的#
以nums=[1,2,3]为例,我们一步步拆解回溯过程:
Step 1: 选择第一个数字
- 可选1/2/3
- 假设选择1,当前路径[1]
Step 2: 选择第二个数字
- 剩下可选2/3
- 选择2,路径变为[1,2]
Step 3: 选择第三个数字
- 只剩3,得到完整排列[1,2,3]
- 回溯到上一步
Step 4: 撤销第二步的选择
- 路径回到[1],重新选择3
- 路径变为[1,3]
Step 5: 选择第三个数字
- 只剩2,得到排列[1,3,2]
- 继续回溯…
直到遍历所有可能的路径!
⚡ 代码实现:回溯解法#
class Solution {
// 主函数:返回 nums 的所有全排列
public List<List<Integer>> permute(int[] nums) {
// res:用于保存最终所有排列结果
List<List<Integer>> res = new ArrayList<>();
// 启动回溯:
// - used 用于标记每个下标对应的数字是否已被当前路径使用
// - path 记录当前正在构建的排列
// - res 收集完整排列
backtrack(nums, new boolean[nums.length], new ArrayList<>(), res);
// 返回所有收集到的排列
return res;
}
// 回溯函数:在当前 path 基础上继续尝试未使用的数字
private void backtrack(int[] nums, boolean[] used,
List<Integer> path, List<List<Integer>> res) {
// 终止条件:当路径长度等于数组长度时,说明形成了一个完整排列
if (path.size() == nums.length) {
// 注意要拷贝一份 path 再加入结果,避免后续回溯修改影响已保存结果
res.add(new ArrayList<>(path));
// 当前分支完成,返回上一层继续尝试其他选择
return;
}
// 枚举每一个数字下标,尝试把还没用过的数字放到当前位置
for (int i = 0; i < nums.length; i++) {
// 剪枝:如果 nums[i] 已在当前路径中使用,则跳过
if (used[i]) continue;
// 做选择:把 nums[i] 放入当前路径,并标记为已使用
used[i] = true;
path.add(nums[i]);
// 递归进入下一层:继续填下一个位置
backtrack(nums, used, path, res);
// 撤销选择(回溯):
// 1) 删除路径最后一个数字
// 2) 恢复 used 状态,便于后续分支复用该数字
path.remove(path.size() - 1);
used[i] = false;
}
}
}java🎯 算法要点解析#
回溯法的精髓在于:
- 路径选择:记录已经做出的选择
- 选择列表:当前可选的元素集合
- 终止条件:到达决策树的底层
- 状态重置:回到上一层前的清理操作
📊 复杂度分析#
时间复杂度:O(n×n!)
- 排列总数是n!种
- 每个排列需要O(n)时间复制到结果集
空间复杂度:O(n)
- 递归栈深度最大为n
- 使用布尔数组记录访问状态
🎯 面试官最爱追问#
Q:如果数组包含重复元素怎么办?
A:需要先排序,然后在回溯时跳过相同元素(LeetCode 47题)
Q:如何优化空间复杂度?
A:可以通过交换元素实现原地排列,不需要额外空间记录状态
Q:如何按字典序输出结果?
A:先对数组排序,回溯时按顺序选择元素
💡 举一反三#
这个模板还可以解决:
- 组合总和(LeetCode 39)
- 子集问题(LeetCode 78)
- 电话号码字母组合(LeetCode 17)
- N皇后问题(LeetCode 51)
🎁 思考题#
如果要求返回第k个排列,而不是所有排列,如何用O(n)的时间复杂度解决?
例如: 输入:n=3, k=3 输出:“213”
如果你知道答案,或者有自己的想法?欢迎在评论区留言讨论~
📝 代码模板总结#
回溯算法的通用步骤:
- 定义结果集和路径
- 遍历选择列表
- 做出选择并更新状态
- 递归进入下一层
- 撤销选择并恢复状态