LeetCode 60. 排列序列
题目描述
✅ 60. 排列序列


题意分析
将数字
1..n的所有排列按字典序排列,返回其中从1开始计数的第k个。每个数字恰好使用一次,题目保证1 <= n <= 9且排名合法。不需要生成前面所有排列。若能算出一个前缀下有多少种排列,就可以跳过整段候选,逐位确定目标排列。
解法:阶乘分组定位每一位
核心思路
[!blue]
假设已经固定一段前缀,还剩
remain个未使用的数字。字典序会先比较下一位,所以以最小候选为下一位的排列全部在前,以第二小候选为下一位的排列紧随其后,依此类推。每个候选一旦固定,剩余remain-1个数字可以任意排列,因此每块都有(remain-1)!种,且这些块连续、等大。先执行一次
k--,把从1开始的排名变成从0开始的偏移。令blockSize = (remain-1)!,那么k / blockSize是目标前面完整跳过的块数,也就是当前应选数字在升序候选列表中的下标;k % blockSize则是进入这一块后,目标在块内的偏移。选中数字后,把它加入答案并从候选列表删除,剩下的数字仍保持升序。更新为块内偏移后,问题变成“固定了更长前缀,继续寻找剩余数字的指定排列”,因此可以重复相同过程直到选完所有位。
每轮都有
0 <= k < remain!,所以商一定落在0..remain-1,不会越过候选列表;余数小于(remain-1)!,恰好维持下一轮的范围。最后只剩一个数字时,块大小为0! = 1,偏移必为0,直接取唯一候选即可。
解题步骤
- 预处理
factorial[i] = i!,设置factorial[0] = 1。- 初始化升序候选列表
1..n,并且只在进入循环前执行一次k--。- 从
remain = n逐步减到1,每轮令blockSize = factorial[remain-1]。- 计算
index = k / blockSize,将对应候选加入答案,并从列表删除,保持其余数字的顺序。- 执行
k %= blockSize,继续在选中块内部定位下一位;所有数字选完后返回答案字符串。
代码实现
class Solution {
public String getPermutation(int n, int k) {
int[] factorial = new int[n + 1];
factorial[0] = 1;
for (int i = 1; i <= n; i++) {
factorial[i] = factorial[i - 1] * i;
}
List<Integer> nums = new ArrayList<>();
for (int num = 1; num <= n; num++) {
nums.add(num);
}
StringBuilder ans = new StringBuilder();
// 只在入口转换为零基排名,后续余数已经是块内零基偏移。
k--;
for (int remain = n; remain > 0; remain--) {
int blockSize = factorial[remain - 1];
// 商决定选择哪个候选,余数决定进入该块后的排名。
int index = k / blockSize;
ans.append(nums.remove(index));
k %= blockSize;
}
return ans.toString();
}
}
func getPermutation(n int, k int) string {
factorial := make([]int, n+1)
factorial[0] = 1
for i := 1; i <= n; i++ {
factorial[i] = factorial[i-1] * i
}
nums := make([]int, n)
for i := range nums {
nums[i] = i + 1
}
ans := make([]byte, 0, n)
// 只在入口转换为零基排名,后续余数已经是块内零基偏移。
k--
for remain := n; remain > 0; remain-- {
blockSize := factorial[remain-1]
// 商决定选择哪个候选,余数决定进入该块后的排名。
index := k / blockSize
ans = append(ans, byte('0'+nums[index]))
nums = append(nums[:index], nums[index+1:]...)
k %= blockSize
}
return string(ans)
}
复杂度分析
- 时间复杂度:$O(n^2)$。阶乘预处理为 $O(n)$;共选择 $n$ 次,每次商和余数计算为 $O(1)$,但 Java 数组列表或 Go 切片删除元素需要移动后缀,累计为 $O(n^2)$。
- 空间复杂度:$O(n)$。阶乘表、剩余候选与答案的大小都与
n同阶。
关键点总结
[!green]
- 相同前缀下,字典序按下一位分成连续块;每块包含剩余位置的全排列,所以大小是阶乘。
k / blockSize决定选哪个数字,k % blockSize决定进入该块后继续找哪个排列。- 每轮删除已选数字并保持候选升序,才能让列表下标始终对应字典序中的块顺序。
- 合法排名经整除和取余后仍然合法,最后一位由
0! = 1自然处理。
易错点总结
[!yellow]
- 忘记入口处的
k--,会把块末尾的排列划到下一块;每轮重复减一也不对,取余后的偏移已经从0开始。- 用
factorial[remain]作为块大小:下一位已经固定,每块只有remain-1个位置可排列。- 忘记删除选中的数字,后续会重复使用;删除时打乱候选顺序,则商不再对应正确的字典序块。
- 未设置
factorial[0] = 1,最后一位会出现错误的除数。- 题目限制
n <= 9,所以阶乘能放入int,Go 也能用byte('0'+num)拼出单个数字;这段字符转换依赖该范围。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 46. 全排列 | 中等 | 全排列树每个首选分支含有固定阶乘数量的叶子,本题利用这一数量跳过整个分支。 |
| 47. 全排列 II | 中等 | 逐位选择未使用元素构造排列;本题用阶乘块大小直接定位第 k 个排列,该题排序后增加同层重复剪枝。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!