🌸题目
🍁给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。
按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:
1 | "123" |
给定 n 和 k,返回第 k 个排列。
说明:
- 给定 n 的范围是
[1, 9]
。 - 给定 k 的范围是
[1, n!]
。
示例 1:
1 | 输入: n = 3, k = 3 |
示例 2:
1 | 输入: n = 4, k = 9 |
🌸分析
以下给出了两种方法,思路其实是一样的:通过 计算剩余数字个数的阶乘数,一位一位选出第 k
个排列的数位。
🌸解法一:DFS+剪枝
容易想到,使用同「力扣」第 46 题: 全排列 的回溯搜索算法,依次得到全排列,输出第 k 个全排列即可。事实上,我们不必求出所有的全排列。
基于以下几点考虑:
- 所求排列 一定在叶子结点处得到,进入每一个分支,可以根据已经选定的数的个数,进而计算还未选定的数的个数,然后计算阶乘,就知道这一个分支的 叶子结点 的个数:
- 如果 k 大于这一个分支将要产生的叶子结点数,直接跳过这个分支,这个操作叫「剪枝」;
- 如果 k 小于等于这一个分支将要产生的叶子结点数,那说明所求的全排列一定在这一个分支将要产生的叶子结点里,需要递归求解。
编码注意事项:
- 计算阶乘的时候,可以使用循环计算。注意:0!=1,它表示了没有数可选的时候,即表示到达叶子结点了,排列数只剩下 1个;
- 题目中说「给定 n 的范围是 [1, 9]」,可以把从 0到 9 的阶乘计算好,放在一个数组里,可以根据索引直接获得阶乘值;
- 编码的时候,+1 还是 -1 ,大于还是大于等于,这些不能靠猜。常见的做法是:代入一个具体的数值,认真调试。
1 | import java.util.Arrays; |
python
1 | class Solution: |
时间复杂度:O({N^2}),
空间复杂度:O(N),nums、used、path 都与 N 等长,factorial 数组就 10个数,是常数级别的。
k -= cnt;
这一步,每一次剪枝太慢,事实上,可以用除法加快这一步骤。但是需要维护一个有序数组(或者链表),时间复杂度不变。
🌸解法二:有序数组(链表)模拟
以 n = 4
,k = 6
,为例,现在确定第 1 个数字填啥。如果第 k
个数恰好是后面的数字个数的阶乘,那么第 1 个数字就只能填最小的 1。
如果 n = 4
,k = 16
,现在确定第 1 个数字填啥。如果 k
> 后面的数字个数的阶乘。数一数,可以跳过几个阶乘数。
其实这个思路很像方法一的「剪枝」,只不过方法一就减法,方法二用除法。事实上,方法二要维护数组的有序性,所以时间复杂度不变。根据以上思路,设计算法流程如下:
算法流程设计:
- 把候选数放在一个 有序列表 里,从左到右根据「剩下的数的阶乘数」确定每一位填谁,公式 k / (后面几位的阶乘数) 的值 恰好等于候选数组的下标;
- 选出一个数以后,k 就需要减去相应跳过的阶乘数的倍数;
- 已经填好的数需要从候选列表里删除,注意保持列表的有序性(因为排列的定义是按照字典序);
- 由于这里考虑的是下标,第 k 个数,下标为
k - 1
,一开始的时候,k–。
每次选出一个数,就将这个数从列表里面拿出。这个列表需要支持频繁的删除操作,因此使用双链表。在 Java 中LinkedList
就是使用双链表实现的。
1 | import java.util.LinkedList; |
题解出自@liweiwei1419