LeetCode 第60题:排列序列
题目描述
给出集合 [1,2,3,...,n],其所有元素共有 n! 种排列。
按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:
- "123"
- "132"
- "213"
- "231"
- "312"
- "321"
给定 n 和 k,返回第 k 个排列。
难度
困难
题目链接
示例
示例 1:
输入:n = 3, k = 3
输出:"213"
示例 2:
输入:n = 4, k = 9
输出:"2314"
示例 3:
输入:n = 3, k = 1
输出:"123"
提示
1 <= n <= 91 <= k <= n!
解题思路
方法:数学 + 递归
这道题如果使用回溯法生成所有排列再取第k个会超时,我们需要用数学的方法直接计算第k个排列。
关键点:
- 每个位置的数字可以通过 k 和阶乘数推导出来
- 使用阶乘系统来定位每一位的数字
- 维护一个数字列表,记录可用的数字
- 每确定一位就将对应数字从列表中移除
具体步骤:
- 计算n的阶乘数组
- 将k减1(因为排列从1开始计数)
- 对于每一位(从左到右):
- 计算当前位的数字应该是剩余数字中的第几个
- 将这个数字加入结果
- 从可用数字列表中移除这个数字
- 更新k值
- 返回最终的字符串
时间复杂度:O(n²),主要是移除数字的操作 空间复杂度:O(n),需要存储阶乘数组和可用数字列表
代码实现
C# 实现
public class Solution {
public string GetPermutation(int n, int k) {
// 计算阶乘数组
int[] factorial = new int[n];
factorial[0] = 1;
for (int i = 1; i < n; i++) {
factorial[i] = factorial[i - 1] * i;
}
// 创建可用数字列表
List<int> numbers = new List<int>();
for (int i = 1; i <= n; i++) {
numbers.Add(i);
}
k--; // 将k转换为0-based索引
StringBuilder sb = new StringBuilder();
// 对于每一位
for (int i = n - 1; i >= 0; i--) {
int index = k / factorial[i];
k %= factorial[i];
// 添加当前位的数字
sb.Append(numbers[index]);
numbers.RemoveAt(index);
}
return sb.ToString();
}
}
执行结果
- 执行用时:84 ms
- 内存消耗:36.5 MB
代码亮点
- 🎯 使用数学方法直接计算,避免生成所有排列
- 💡 预计算阶乘数组,提高效率
- 🔍 使用List来维护可用数字,方便删除操作
- 🎨 使用StringBuilder构建结果字符串
常见错误分析
- 🚫 没有将k转换为0-based索引
- 🚫 阶乘计算错误
- 🚫 数字删除顺序错误
- 🚫 没有正确处理边界情况
图解思路
示例分析(n=4, k=9)
初始状态:
可用数字:[1,2,3,4]
k = 8 (转换为0-based)
阶乘数组:[1,1,2,6]
第1位:
k = 8, factorial[3] = 6
index = 8 / 6 = 1
选择数字2,剩余[1,3,4]
k = 8 % 6 = 2
第2位:
k = 2, factorial[2] = 2
index = 2 / 2 = 1
选择数字3,剩余[1,4]
k = 2 % 2 = 0
第3位:
k = 0, factorial[1] = 1
index = 0 / 1 = 0
选择数字1,剩余[4]
k = 0 % 1 = 0
第4位:
k = 0, factorial[0] = 1
index = 0 / 1 = 0
选择数字4,剩余[]
最终结果:"2314"
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 回溯法 | O(n! * n) | O(n) | 思路简单 | 效率极低 |
| 数学法 | O(n²) | O(n) | 效率高 | 实现复杂 |
相关题目
- LeetCode 31. 下一个排列 - 中等
- LeetCode 46. 全排列 - 中等
- LeetCode 47. 全排列 II - 中等