1. 问题背景与核心挑战
LeetCode 60题"排列序列"要求我们找出集合[1,2,...,n]按字典序排列的第k个全排列。这道题看似简单,但隐藏着几个关键难点:
当n=4时,所有排列按字典序为:
1 "1234"
2 "1243"
3 "1324"
4 "1342"
5 "1423"
6 "1432"
7 "2134"
...
24 "4321"
直接暴力生成所有排列然后取第k个的方法在n较大时(比如n=9时共有362880种排列)会面临严重的性能问题。我们需要找到一种数学方法,能够不生成全部排列就能定位到第k个排列。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数学推导与规律发现
2.1 阶乘分组原理
观察n=4的情况可以发现,排列被自然地分成了几组:
- 前6个(1-6)以1开头
- 接下来6个(7-12)以2开头
- 然后6个(13-18)以3开头
- 最后6个(19-24)以4开头
这是因为对于固定的第一个数字,后面三个数字有3! = 6种排列方式。这个规律可以推广到一般情况:对于n个数字的第k个排列,我们可以通过计算(k-1)//(n-1)!来确定第一个数字的位置。
2.2 递归关系建立
确定第一个数字后,问题就转化为在剩下的n-1个数字中找第k'个排列,其中k' = k - (第一个数字的位置-1)*(n-1)!。这个过程可以递归进行,直到只剩下一个数字。
例如,找n=4,k=9的排列:
- (9-1)//6 = 1 → 第一个数字是nums[1]=2
- 新k=9-1*6=3
- 剩余数字[1,3,4]
- (3-1)//2=1 → 第二个数字是nums[1]=3
- 新k=3-1*2=1
- 剩余数字[1,4]
- (1-1)//1=0 → 第三个数字是nums[0]=1
- 最后剩下4
最终排列是"2314"
3. Python实现与剪枝优化
3.1 基础实现
python复制def getPermutation(n: int, k: int) -> str:
factorials = [1]
for i in range(1, n):
factorials.append(factorials[-1] * i)
k -= 1
nums = list(range(1, n+1))
res = []
for i in range(n-1, -1, -1):
index = k // factorials[i]
res.append(str(nums.pop(index)))
k -= index * factorials[i]
return ''.join(res)
3.2 关键优化点
- 预计算阶乘值:提前计算好0!到(n-1)!的值,避免重复计算
- 索引调整:将k减1转换为0-based索引,简化计算
- 动态维护数字列表:使用pop操作移除已使用的数字,确保每次都在剩余数字中选择
- 迭代而非递归:使用循环而非递归,避免函数调用开销
4. 复杂度分析与对比
4.1 暴力法复杂度
- 时间复杂度:O(n×n!),需要生成所有排列
- 空间复杂度:O(n!),需要存储所有排列
4.2 数学推导法复杂度
- 时间复杂度:O(n²),外层循环n次,pop操作平均O(n)
- 空间复杂度:O(n),只需要存储阶乘数组和数字列表
当n=9时:
- 暴力法需要生成362880个排列
- 数学法只需最多81次操作
5. 边界条件与注意事项
5.1 特殊输入处理
- n=1时直接返回"1"
- k=1时直接返回升序排列
- k=n!时直接返回降序排列
5.2 常见错误
- 忘记k减1转换为0-based索引
- 阶乘数组计算错误(应从0!开始)
- 数字列表维护不当(应确保每次选择后更新列表)
- 整数除法与取模运算混淆
5.3 调试技巧
- 对于n=4,手动计算几个k值验证结果
- 打印中间变量(如当前的index、k值、剩余数字列表)
- 使用小n值测试极端情况(k=1和k=n!)
6. 扩展思考与变种问题
6.1 变种问题
- 找出所有排列中比给定排列小的排列数量
- 给定一个排列,找出它在字典序中的位置
- 实现前一个排列和后一个排列的函数
6.2 性能优化空间
- 使用链表结构优化pop操作
- 对于极大n值(如n>20),需要考虑大整数运算
- 并行计算阶乘值(虽然对n≤9意义不大)
提示:在实际面试中,除了写出正确代码外,能够清晰解释数学原理和优化思路同样重要。建议在编码前先与面试官讨论解题思路。
7. 完整测试用例
python复制test_cases = [
(1, 1, "1"),
(3, 3, "213"),
(4, 9, "2314"),
(4, 24, "4321"),
(5, 16, "14352"),
(9, 362880, "987654321")
]
for n, k, expected in test_cases:
assert getPermutation(n, k) == expected
通过这道题,我们学到了如何将看似需要暴力枚举的问题转化为数学计算问题。这种思维方式在解决许多算法问题时都非常有用,特别是当问题规模较大时,寻找数学规律往往能带来数量级的性能提升。
