【LeetCode】46. 全排列
算法思路
采用回溯算法解决全排列问题,核心思想是递归探索所有可能的排列路径:
- 路径记录:维护当前已选择的元素路径
- 选择列表:通过标记数组记录可用元素,确保每个元素只使用一次
- 递归终止条件:当路径长度等于原数组长度时,记录完整排列
实现代码
func permute(nums []int) [][]int { res := [][]int{} visited := make([]bool, len(nums)) path := []int{} var backtrack func() backtrack = func() { // 终止条件:路径长度等于数组长度 if len(path) == len(nums) { tmp := make([]int, len(path)) copy(tmp, path) res = append(res, tmp) return } // 遍历选择列表 for i := 0; i < len(nums); i++ { if visited[i] { // 跳过已使用的元素 continue } // 做选择 visited[i] = true path = append(path, nums[i]) // 进入下一层决策树 backtrack() // 撤销选择 visited[i] = false path = path[:len(path)-1] } } backtrack() return res }
代码解析
-
初始化结构
visited
数组标记元素是否已被使用path
切片记录当前路径状态
-
递归核心逻辑
- 终止条件:当路径长度等于原数组长度时,复制当前路径到结果集
- 遍历选择:每次选择未使用的元素加入路径,并标记为已访问
- 回溯操作:递归返回后撤销当前选择,恢复标记状态
-
避免引用陷阱
使用copy(tmp, path)
深拷贝路径,防止后续操作修改已存储结果
复杂度分析
指标 | 值 | 说明 |
---|---|---|
时间复杂度 | O(n×n!) | 需要生成n!个排列,每个排列复制耗时O(n) |
空间复杂度 | O(n) | 递归栈深度最大为n,标记数组占用O(n) |
示例验证
func main() { // 示例1:[1,2,3] fmt.Println(permute([]int{1,2,3})) // 输出6种排列组合 // 示例2:[0,1] fmt.Println(permute([]int{0,1})) // 输出[[0,1],[1,0]] // 示例3:[1] fmt.Println(permute([]int{1})) // 输出[[1]] }
关键优化点
-
标记数组代替删除操作
相比通过删除元素生成子数组的方式,标记数组可避免频繁切片操作带来的性能损耗 -
闭包实现回溯
利用Golang闭包特性直接访问外层变量,简化参数传递逻辑 -
内存预分配优化
可预先计算排列总数factorial(len(nums))
,初始化结果集容量以减少扩容开销
与其他方法对比
方法 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
回溯标记法 | 时间复杂度最优 | 需要额外标记数组 | 标准全排列场景 |
元素交换法 | 节省标记数组空间 | 破坏原数组顺序 | 内存敏感场景 |
库函数实现 | 代码简洁 | 依赖语言特性 | 快速验证场景 |
边界处理
- 空数组处理:当输入
nums
为空时直接返回空切片 - 单元素数组:直接返回
[[元素值]]
- 大数阶乘优化:当n较大时需要考虑内存分配策略
总结
该实现基于标准回溯算法,通过标记数组管理选择列表,能够高效生成全排列结果。