题目描述
给出集合 [1,2,3,…,n]
,其所有元素共有 n! 种排列。
按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:
"123"
"132"
"213"
"231"
"312"
"321"
给定 n 和 k,返回第 k 个排列。
说明:
给定 n 的范围是 [1, 9]。
给定 k 的范围是[1, n!]。
示例 1:
输入: n = 3, k = 3
输出: "213"
示例 2:
输入: n = 4, k = 9
输出: "2314"
来源:https://leetcode-cn.com/problems/permutation-sequence
题目解析
一道比较偏数学的题目。
题意是给定 1 ~ n 的数字(n <= 9),因为数字所在的位置不同,这些数字可以组成的整数也不同。最后让你求按照组成的整数大小排序,排在第 k 的整数是多少。
要求全排列的话,时间和空间复杂度都是 O(n!)
。
先看最直接的解法,通过暴力搜索,找出所有的全排列的情况,然后排序,直接就可以得出答案。
这么做的时间复杂度是 O(n!log(n!))
。
很显然的是,这么做时间上面不符合要求,需要进一步优化算法。
首先考虑的一个问题是,我们需不需要找出所有的组合情况?
如果要找出所有的情况,那么时间上肯定是没法提高的。这其实就涉及到一个暴力搜索的剪支的问题,就拿 n = 3 为例,按照元素大小,全排列如下:
"123"
"132"
"213"
"231"
"312"
"321"
这里,如果说 k = 5,那么是不是可以说以 “1” 和 “2” 开始的情况我们就不需要考虑了?
我们只需要从 “3” 开始的序列开始找就可以了,这样子可以很大程度上节省搜索的成本。
那么问题来了,如何确定我们需要找的结果在不在以某个数字开头的区间内?
其实,每个数字开头的序列总个数都是一样的,比如上面的以 “1”,“2”,“3” 开头的序列个数都是 2 个,我们只需要逐个排除就行了。
如果 k = 5,因为 5 > 2,说明结果不会在以 “1” 开头的区间中。又因为 5 > 4,说明结果不会在以 “2” 开头的区间中。
但是 5 < 6 的,所以我们需要在开头是 “3” 的序列中继续找。确定了开头数字是 3 之后,我们可以把 3 排除,然后继续去用同样的方法确定第二个数字,第三个数字。。。
这样做下来,时间上面是可以大大提高的。至于具体的时间复杂度是多少,这和具体的输入有关。但是空间方面,我们并不需要 O(n!)
的空间,n 的空间(函数栈空间)就足够了。
参考代码
func getPermutation(n int, k int) string {
nums := []int{}
for i := 1; i <= n; i++ {
nums = append(nums, i)
}
return helper(nums, k)
}
func helper(nums []int, k int) string {
totalNums := 1
// 计算以每个数字开头的序列个数
for i := 1; i < len(nums); i++ {
totalNums *= i
}
for i := range nums {
// 以当前数字开头的话,前面的序列的总个数
sum := (i + 1) * totalNums
sub := k - sum
// 说明结果是以当前数字开头
// 去掉当前选中的数字,递归去确定接下来的数字
if sub <= 0 {
arr := []int{}
arr = append(arr, nums[:i]...)
arr = append(arr, nums[i+1:]...)
return strconv.Itoa(nums[i]) + helper(arr, k - i * totalNums)
}
}
return ""
}