题目链接
题目描述
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
解题思路
这是一个高中的排列组合问题,我们可以先想一想当时的我们是怎么做的,很明显就是穷举。
1,2,3,4
1,2,4,3
1,3,2,4
…
如此下去,其实就是用回溯法剪枝的方法遍历一棵二叉树,或者说深度优先遍历,是一个意思。
确定用回溯剪枝的方法后,就可以套用回溯剪枝的框架了
result = []
func backtrack(路径,选择列表) {
if 满足结束条件 {
result.add(路径)
}
return
for 选择 in 选择列表 {
做选择
backtrack(路径,选择列表)
撤销选择
}
}
想清楚几个问题
- 结束条件是什么?(所有数字都用了一遍)
- 怎么定义走过的路径?(用一个数组保存一种排列方式)
- 如何做选择?(在给出的数字列表里面依次选)
想清楚这几个问题就可以开始写代码了。
题解
// 返回的全排列结果
var res [][]int
func permute(nums []int) [][]int {
// 注意:这里要重置一下全局数组(leetcode多次执行全局变量不会消失),防止多个测试用例的结果都放到一起
res = [][]int{}
// 用一个数组维护走过的路径
visited := make(map[int]bool)
// 用 path 保存当前路径
var path []int
// 回溯剪枝
backTrack(nums, path, visited)
return res
}
func backTrack(nums, path []int, visited map[int]bool) {
// 先判断结束条件, 走过的路径包含了所有的选择,说明已经得到一种排列方式
if len(nums) == len(path) {
// 注意:这里不能直接append,因为切片底层共用数据,
// 这意味着下面切片path一旦改变了,res也会随之改变,而这不是我们希望看到的
// 所以只能重新开辟一个新的切片将内容拷贝过去
tmp := make([]int, len(nums))
copy(tmp, path)
res = append(res, tmp)
return
}
// 遍历选择列表,做选择
for _, num := range nums {
// 走过的分支直接跳过,直接走下一个分支,避免进入死循环
if visited[num] {
continue
}
// 将这个选择加入当前路径
path = append(path, num)
// 将这个选择置为已访问
visited[num] = true
// 进入下一层决策树
backTrack(nums, path, visited)
// 退出决策树,回退,把这个选择从路径中移除
path = path[:len(path)-1]
// 将这个选择重新置为未访问
visited[num] = false
}
}
复杂度分析
时间复杂度:O((n * n!))
其中 n 为序列的长度。
算法的复杂度首先受 backtrack 的调用次数制约, 这说明 backtrack 的调用次数是 O((n!)) 的。
而对于 backtrack 调用的每个叶结点(共 n! 个),我们需要将当前答案使用 O((n)) 的时间复制到答案数组中,相乘得时间复杂度为 O((n * n!))。
空间复杂度:O((n))
其中 n 为序列的长度。除答案数组以外,递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,这里可知递归调用深度为 O((n))
反思
这道题有两个需要着重注意的点,特别容易错,我自己做的时候在这里卡了很久:
- 如果定义了全局变量,需要注意要重置一下全局变量,否则多个测试用例的结果会被放到一起,因为leetcode多次执行全局变量不会消失
- 当获得一个排列时时,不能直接append,因为切片底层共用数据,这意味着下面如果切片path一旦改变了,我们的res也会随之改变,而这不是我们希望看到的,所以只能重新开辟一个新的切片将内容拷贝过去,这样就保证了path变的时候res不会跟着变