1. 问题背景与理解
"第k个排列"是一个经典的算法问题,它要求我们找到给定数字集合的字典序排列中的第k个排列。比如对于数字1、2、3,它们的全排列按字典序排列为:
- 123
- 132
- 213
- 231
- 312
- 321
如果k=4,那么我们需要返回"231"。这个问题看似简单,但当n较大时(比如n=9),全排列的数量会爆炸式增长(9! = 362880),直接生成所有排列再取第k个显然不现实。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数学规律分析
解决这个问题的关键在于发现排列的数学规律。我们可以观察到:
- 对于n个数字的全排列,以某个数字开头的排列共有(n-1)!个
- 因此,第k个排列的第一个数字可以通过k/(n-1)!来确定
- 确定第一个数字后,问题就转化为在剩下的数字中找第k%(n-1)!个排列
举个例子,n=3,k=4:
- 第一位的选择:每个数字开头的排列有2! = 2个
- k=4 > 2*1,所以第一位是2(数字排序为1,2,3)
- 然后在剩下的数字1,3中找第4-2=2个排列,即"31"
- 最终结果为"231"
3. Java实现详解
java复制public class Solution {
public String getPermutation(int n, int k) {
List<Integer> numbers = new ArrayList<>();
int[] factorial = new int[n+1];
StringBuilder sb = new StringBuilder();
// 创建阶乘查找表
factorial[0] = 1;
for (int i = 1; i <= n; i++) {
factorial[i] = factorial[i-1] * i;
}
// 创建数字列表
for (int i = 1; i <= n; i++) {
numbers.add(i);
}
k--; // 转换为0-based索引
for (int i = 1; i <= n; i++) {
int index = k / factorial[n-i];
sb.append(numbers.remove(index));
k -= index * factorial[n-i];
}
return sb.toString();
}
}
关键点说明:
- 预先计算阶乘值,避免重复计算
- 使用ArrayList存储可用数字,便于根据索引快速移除元素
- k--是因为我们想要0-based索引,便于计算
- 每次确定一位数字后,从列表中移除该数字
时间复杂度:O(n^2)(因为ArrayList的remove操作是O(n)的)
空间复杂度:O(n)
4. JavaScript实现详解
javascript复制function getPermutation(n, k) {
let numbers = [];
let factorial = new Array(n+1).fill(1);
let result = [];
// 计算阶乘
for (let i = 1; i <= n; i++) {
factorial[i] = factorial[i-1] * i;
numbers.push(i);
}
k--; // 转换为0-based
for (let i = 1; i <= n; i++) {
const index = Math.floor(k / factorial[n-i]);
result.push(numbers[index]);
numbers.splice(index, 1);
k -= index * factorial[n-i];
}
return result.join('');
}
JS实现注意事项:
- 使用数组的splice方法移除元素,这与Java的ArrayList.remove类似
- 注意JS中整数除法需要使用Math.floor
- 最终使用join将数组转换为字符串
5. Python实现详解
python复制def getPermutation(n: int, k: int) -> str:
from math import factorial
numbers = list(range(1, n+1))
result = []
k -= 1 # 转换为0-based
for i in range(n, 0, -1):
fact = factorial(i-1)
index = k // fact
result.append(str(numbers.pop(index)))
k %= fact
return ''.join(result)
Python实现特点:
- 使用math库的factorial函数,避免重复计算
- 列表的pop操作效率较高
- 使用字符串拼接生成最终结果
6. 算法优化与边界处理
6.1 边界条件处理
- n=1时直接返回"1"
- k不能超过n!,需要做校验
- k=1时直接返回升序排列
6.2 阶乘计算的优化
可以预先计算所有可能的阶乘值并存储,避免重复计算:
python复制factorials = [1] * (n+1)
for i in range(1, n+1):
factorials[i] = factorials[i-1] * i
6.3 数字选择优化
可以使用位运算或更高效的数据结构来跟踪剩余数字,但实际测试中简单的列表操作已经足够高效。
7. 测试用例设计
好的测试用例应该包括:
- 最小情况:n=1, k=1
- 最大k值:n=3, k=6
- 一般情况:n=4, k=9
- 边界情况:n=9, k=362880
- 随机情况:n=5, k=34
示例测试:
java复制@Test
public void testGetPermutation() {
Solution s = new Solution();
assertEquals("123", s.getPermutation(3, 1));
assertEquals("132", s.getPermutation(3, 2));
assertEquals("213", s.getPermutation(3, 3));
assertEquals("231", s.getPermutation(3, 4));
assertEquals("312", s.getPermutation(3, 5));
assertEquals("321", s.getPermutation(3, 6));
}
8. 实际应用场景
这种算法在以下场景中有实际应用:
- 密码破解中尝试特定位置的排列组合
- 游戏开发中生成特定序列的关卡或道具排列
- 数据分析中对样本进行特定顺序的排列测试
- 教学演示排列组合的数学概念
9. 常见错误与调试技巧
-
忘记k--转换为0-based索引:
- 症状:结果总是比预期大一位
- 调试:检查k是否在计算前减1
-
阶乘计算错误:
- 症状:当n较大时结果不正确
- 调试:验证阶乘数组的值是否正确
-
数字列表更新不及时:
- 症状:结果中出现重复数字
- 调试:确保每次选择后从列表中移除已选数字
-
整数溢出:
- 症状:大n值时结果异常
- 调试:使用long类型存储阶乘值(Java)
10. 算法扩展与变种
-
有重复元素的第k个排列:
- 需要调整阶乘计算,考虑重复元素的排列数减少
- 例如:数字1,1,2的排列只有3种而非6种
-
获取前k个排列:
- 可以修改算法连续生成多个排列
- 但效率不如直接生成前k个排列的专用算法
-
逆问题:给定排列求它是第几个:
- 可以使用类似的数学原理反向计算
- 对每位数字,计算它在其后数字中的排名
在实际面试中,这类问题经常被用来考察候选人对数学规律的发现能力和编码实现能力。理解其背后的数学原理比记忆代码更重要。
