文章目录
46.全排列
题目:
给定一个不含重复数字的数组 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 中的所有整数 互不相同
思路:
给定一个不含重复数字的数组,我们需要求出它的所有全排列。
全排列问题本质上是枚举所有可能的排列组合 ,并且每个元素只能使用一次,非常适合用回溯算法解决。
回溯算法的核心逻辑可以概括为:
一步步做选择 → 递归往下走 → 走到底后回退一步 → 换一个选择再试
可以把构造全排列的过程,想象成给 n 个位置依次填数字的游戏:
- 从第一个位置开始,依次尝试所有还没有用过的数字
- 选中一个数字后,将它放入当前路径,并标记为已使用,避免重复选择
- 递归进入下一个位置,继续选择数字
- 当所有位置都填满时,说明得到了一个完整排列,将其保存到结果集中
- 回退(回溯):撤销上一步的选择,取消标记,尝试其他可能的数字
这就是回溯的灵魂:
选择 → 递归 → 撤销选择
需要维护的三个关键变量:
result:最终结果集合,用于存储所有完整的全排列path:当前路径,保存正在构造中的排列used:布尔标记数组,记录哪些数字已经被使用,防止重复选取
本质:
在递归的每一层 中,选择一个尚未使用的数字,加入当前路径,不断向下构造,直到形成完整排列。
例如 nums = [1,2,3]
整个回溯过程可以用一棵递归树直观表示:
[]
├── [1]
│ ├── [1,2]
│ │ └── [1,2,3]
│ └── [1,3]
│ └── [1,3,2]
├── [2]
│ ├── [2,1]
│ │ └── [2,1,3]
│ └── [2,3]
│ └── [2,3,1]
└── [3]
├── [3,1]
│ └── [3,1,2]
└── [3,2]
└── [3,2,1]
每一条从根节点到叶子节点的路径,都是一个合法的全排列
代码实现(Go):
go
package main
import "fmt"
// permute 函数:返回nums的全排列
// nums: 输入的不含重复数字的数组
// 返回值: 存储所有全排列的二维切片
func permute(nums []int) [][]int {
// 1. 定义最终结果集合,用于存储所有完整的排列
var result [][]int
// 2. 定义当前路径path,保存正在拼接的排列
var path []int
// 3. 定义used数组,标记nums中对应下标的数字是否被使用过
// used[i] = true 表示nums[i]已经在path中,不能重复选择
used := make([]bool, len(nums))
// 4. 定义回溯函数(闭包),无参数无返回值
// 在 Go 中递归闭包需要先用 var 声明函数变量,再进行赋值。因为函数体内部要调用自身时,必须保证这个变量在作用域中已经存在。如果直接使用 := 定义匿名函数,函数体执行时变量还未完成绑定,会导致编译错误。
var backtrack func()
backtrack = func() {
// 递归终止条件:当前路径长度等于nums长度,说明已经形成一个完整排列
if len(path) == len(nums) {
// Go 中的切片是引用类型,path 在回溯过程中会不断被修改(追加、回撤)
// 如果不复制直接将 path 存入结果集,结果集中保存的只是指向 path 底层数组的引用
// 后续回溯修改 path 时,已经存入结果集的数据会同步被覆盖,导致最终输出错误
// 因此必须通过 make+copy 创建一个新切片,复制当前 path 的内容再保存,保证结果不会被修改
temp := make([]int, len(path))
copy(temp, path)
// 将复制后的排列加入结果集
result = append(result, temp)
return
}
// 遍历所有数字,尝试每一个可选的数字
for i := 0; i < len(nums); i++ {
// 如果当前数字已经被使用,跳过
if used[i] {
continue
}
// ------------------- 做选择 -------------------
// 标记该数字已使用
used[i] = true
// 将数字加入当前路径
path = append(path, nums[i])
// 递归:继续选择下一个数字,构造排列的下一位
backtrack()
// ------------------- 撤销选择(回溯) -------------------
// 从路径中移除最后一个数字
path = path[:len(path)-1]
// 取消标记,该数字可以被后续选择使用
used[i] = false
}
}
// 启动回溯
backtrack()
// 返回所有全排列结果
return result
}
// main函数:程序入口,用于测试输入输出
func main() {
nums := []int{1, 2, 3}
res := permute(nums)
fmt.Println(res) // [[1 2 3] [1 3 2] [2 1 3] [2 3 1] [3 1 2] [3 2 1]]
}
时间复杂度: O ( n × n ! ) O(n \times n!) O(n×n!)
- 一共 n ! n! n! 种排列
- 每种排列需要构造长度为 n n n 的路径
- 所以总复杂度为 O ( n × n ! ) O(n \times n!) O(n×n!)
空间复杂度: O ( n ) O(n) O(n)
- 递归调用栈深度:最多 n n n 层(数组长度)
path和used数组空间都是 O ( n ) O(n) O(n)- 不计入结果存储的额外空间,复杂度为 O ( n ) O(n) O(n)
回溯的核心
go
used[i] = true // 选这个数
path = append(...) // 放进path
backtrack() // 继续选下一个数
path = pop() // 把数退回来
used[i] = false // 取消选中
这就是标准回溯模板,所有排列组合问题都通用