---
tags: ["LeetCode", "回溯", "数组", "中等"]
create time: 2026-05-17 10:00
---
# 55-全排列
## 题面
> **LeetCode 46. Permutations**
给定一个不含重复数字的数组 `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] <= 10`
- `nums` 中的所有整数 **互不相同**
---
## 思路
> [!question] 💡 思考
手动列出 `[1, 2, 3]` 的所有排列——你会怎么列?
大概率是:先选 1 开头(剩两个位置自由排列),再选 2 开头……每一步从剩余元素中选一个填到当前位置,然后递归处理下一个位置。
这正是回溯算法的经典场景:**枚举一棵决策树的所有完整路径**。
> [!abstract] 🎯 核心洞察
全排列的本质是"从 n 个元素中选 n 个放入序列",分解为 `n` 层决策:
| 决策层 | 任务 | 可选元素 |
|--------|------|---------|
| 第 1 层 | 选第 1 个位置的数 | 全部 n 个 |
| 第 2 层 | 选第 2 个位置的数 | 除去已选的 |
| ... | ... | ... |
| 第 n 层 | 选最后一个 | 仅剩 1 个 |
每层选择数递减:`n × (n-1) × ... × 1 = n!`,所以时间复杂度必然是 O(n!)。
### 回溯框架三要素
任何回溯问题都可以归纳为三个操作:
```
选择 → 递归 → 撤销选择(回溯)
```
- **选择**:做出当前决策,进入下一层。
- **递归**:在已选基础上继续探索。
- **撤销**:回退上一步,尝试其他分支。
维护一个 `path` 记录当前路径,用一个 `used[]` 布尔数组标记哪些元素已被选中——遍历时跳过已用的即可。
### 回溯决策树(以 `nums = [1, 2, 3]` 为例)
```mermaid
flowchart TD
root["空 []"] --> L1a["选 1\n[1]"]
root --> L1b["选 2\n[2]"]
root --> L1c["选 3\n[3]"]
L1a --> L2a["选 2\n[1,2]"]
L1a --> L2b["选 3\n[1,3]"]
L1b --> L2c["选 1\n[2,1]"]
L1b --> L2d["选 3\n[2,3]"]
L1c --> L2e["选 1\n[3,1]"]
L1c --> L2f["选 2\n[3,2]"]
L2a --> Leaf1["[1,2,3] ✅"]
L2b --> Leaf2["[1,3,2] ✅"]
L2c --> Leaf3["[2,1,3] ✅"]
L2d --> Leaf4["[2,3,1] ✅"]
L2e --> Leaf5["[3,1,2] ✅"]
L2f --> Leaf6["[3,2,1] ✅"]
classDef leaf fill:#90EE90,stroke:#228B22,color:#000;
class Leaf1,Leaf2,Leaf3,Leaf4,Leaf5,Leaf6 leaf;
```
### 两种实现方式
> [!tip] 🔑 方法一:`used` 数组(⭐ 推荐)
每层从头遍历所有元素,通过 `used[]` 跳过已选。这是最标准的回溯模板,清晰易懂。
> [!note] 🔑 方法二:原地交换(进阶)
交换 `nums[i]` 和 `nums[start]` 等价于"选 nums[i] 放在 start 位置",递归后恢复交换即撤销。省去了 `used[]` 和 `path[]`,空间更紧凑,但去重时不如方法一方便。**面试中作为加分项**。
**何时用哪种?**
| 场景 | 推荐 |
|------|------|
| 首次学习 / 面试快速写对 | **方法一**(used 数组) |
| 面试官追问 O(1) 额外空间 | **方法二**(原地交换) |
| 含重复元素的排列(LC 47) | **方法一** + 排序去重 |
- **时间复杂度**:O(n × n!) — 共 n! 条完整路径,每条拷贝需 O(n)
- **空间复杂度**:O(n) — 递归栈深度
---
## 代码提示
> [!abstract] 📝 回溯通用骨架
```go
var result [][]int
var dfs func(path []int)
dfs = func(path []int) {
// Base Case: 路径已满
if len(path) == len(nums) {
result = append(result, append([]int(nil), path...)) // ⚠️ 必须拷贝
return
}
for i := 0; i < len(nums); i++ {
if used[i] {
continue // 剪枝:跳过已选元素
}
// 做选择
path = append(path, nums[i])
used[i] = true
// 递归
dfs(path)
// 撤销选择
used[i] = false
path = path[:len(path)-1]
}
}
dfs(nil)
return result
```
---
## 技巧
> [!tip] 🔑 核心模式
全排列是回溯最经典的入门题,掌握后可迁移到大量同类问题:
| 问题类型 | 决策方式 | 代表题目 |
|---------|---------|---------|
| **全排列** | 每层从**全部未用元素**中选 | LC 46(本题) |
| **组合** | 每层从**当前及之后**选(不回头) | LC 77 组合 |
| **子集** | 每个元素**选或不选** | [[回溯/56-子集]] |
> [!step] ⭐ 四步回溯法
```mermaid
flowchart LR
S1["① 定义签名
需要什么参数?"] --> S2["② Base case
何时停止?"]
S2 --> S3["③ for 循环
有哪些选择?"]
S3 --> S4["④ 递归+回溯
选什么?撤什么?"]
S4 --> DONE["✅"]
```
> [!danger] ⚠️ 两个常见陷阱
**① Go 切片不拷贝直接存结果**:`path` 指向同一底层数组,不拷贝的话所有结果会被后续回溯覆盖成同一个值。
```go
// ❌ 错误
result = append(result, path)
// ✅ 正确:创建独立副本
tmp := make([]int, len(path))
copy(tmp, path)
result = append(result, tmp)
// ✅ 或者一行搞定
result = append(result, append([]int(nil), path...))
```
**② 混淆 `for i := range nums` 和 `for i := start; i < len(nums)`**:
- `range nums`(从头遍历 + `used[]`)→ 全排列,允许跳到任意未用元素
- `range start`(从当前位置往后)→ 组合/子集,不允许回头以避免重复
---
## 代码
> [!success] ✅ 方法一:`used` 数组标记(推荐)
```go
// permute 返回 nums 的所有全排列(使用 used 数组标记法)。
//
// 时间复杂度:O(n * n!) — 共 n! 条完整路径,每条 O(n) 拷贝
// 空间复杂度:O(n) — 递归栈深度为 n
func permute(nums []int) [][]int {
var result [][]int
path := make([]int, 0, len(nums))
used := make([]bool, len(nums))
var dfs func()
dfs = func() {
if len(path) == len(nums) {
tmp := make([]int, len(nums))
copy(tmp, path)
result = append(result, tmp)
return
}
for i := 0; i < len(nums); i++ {
if used[i] {
continue
}
path = append(path, nums[i])
used[i] = true
dfs()
used[i] = false
path = path[:len(path)-1]
}
}
dfs()
return result
}
```
> [!success] ✅ 方法二:原地交换
```go
// permuteSwap 使用原地交换实现全排列。
//
// 时间复杂度:O(n * n!)
// 空间复杂度:O(n) — 仅递归栈开销
func permuteSwap(nums []int) [][]int {
var result [][]int
var dfs func(start int)
dfs = func(start int) {
if start == len(nums) {
tmp := make([]int, len(nums))
copy(tmp, nums)
result = append(result, tmp)
return
}
for i := start; i < len(nums); i++ {
nums[start], nums[i] = nums[i], nums[start]
dfs(start + 1)
nums[start], nums[i] = nums[i], nums[start] // 恢复
}
}
dfs(0)
return result
}
```
> [!success] ✅ 单元测试
```go
import (
"slices"
"testing"
)
func TestPermute(t *testing.T) {
tests := []struct {
name string
input []int
expect [][]int
}{
{name: "示例1", input: []int{1, 2, 3}, expect: [][]int{{1,2,3},{1,3,2},{2,1,3},{2,3,1},{3,1,2},{3,2,1}}},
{name: "示例2", input: []int{0, 1}, expect: [][]int{{0,1},{1,0}}},
{name: "单元素", input: []int{1}, expect: [][]int{{1}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := permute(append([]int{}, tt.input...))
slices.SortFunc(got, func(a, b []int) int {
return slices.Compare(a, b)
})
slices.SortFunc(tt.expect, func(a, b []int) int {
return slices.Compare(a, b)
})
if !slices.Equal(got, tt.expect) {
t.Errorf("permute(%v) = %v; want %v", tt.input, got, tt.expect)
}
})
}
}
```
> [!quote] 💬 延伸思考
全排列的决策树是一棵**规则的 n 叉树**——叶子节点数 = n!。这也正是信息论下界:你必须至少访问 n! 个叶子才能列出全部排列,所以 O(n·n!) 已是最优。
> [!summary] 📋 本章要点回顾
> 全排列是回溯的最佳入门题:
>
> 1. **模型简洁**——决策规则对称,便于手绘理解
> 2. **模板通用**——"选择→递归→撤销"可无缝迁移到组合、子集、N 皇后
> 3. **优化阶梯清晰**——从 `used` 数组到原地交换,再到第 K 个排列的数学构造
>
> **口诀**:一进一出两改两回,选了加,回去退。
---
> [!example] 🔀 关联变体题
- **LeetCode 47. Permutations II** — 含重复元素的全排列
- **[[回溯/56-子集]]** — 回溯最基础范式,决策树是二叉树(选或不选)
- **[[回溯/57-电话号码的字母组合]]** — 多叉树搜索,每层候选集不同
- **[[技巧/99-下一个排列]]** — 有序生成排列,不需要枚举全部