60. 第k个排列【中等】
给出集合 [1,2,3,…,n],其所有元素共有 n!种排列。
按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:
- “123”
- “132”
- “213”
- “231”
- “312”
- “321”
给定 n 和 k,返回第 k 个排列。
说明:
- 给定 n 的范围是
[1, 9]。 - 给定 k 的范围是
[1, n!]。
示例 1:
输入: n = 3, k = 3
输出: “213”
示例 2:
输入: n = 4, k = 9
输出: “2314”
思路:深搜+剪枝
由于是输入有序数据:[1..n],那么我们深搜计算[1..n]的全部全排列,会发现是按照字典序排列的,则第k个叶子节点即为所求。
单纯的深搜做了许多无用功,搜索了许多无用节点。可以利用排列的数学性质进行剪枝,以n = 4为例:

定义一个函数f(n,i) = (n-i)! 表示[1..n]的序列中,前i个位置固定的情况下存在的排列数,也就是上图搜索树中每个根节点对应的叶子节点数。
实际上f(n,i)就是(n-i)!,编码时只需预计算1-n的阶乘即可。
如确定第一位为1时,有f(4,1) = 6种排列:234,243,324,342,423,432。
如确定第一位为2时,有f(4,1) = 6种排列:134,143,314,341,413,431。
如确定第一位为3时,有f(4,1) = 6种排列:124,142,214,241,412,421。
如确定第一位为4时,有f(4,1) = 6种排列:123,132,213,231,312,321。
由于第一位的可选元素有4个,所以有4棵子树。(第i位有n-i+1个可选元素)
…
我们的目标是找到k个叶子节点,根据f(n,i)我们确定了当前根节点下的叶子节点数,那么根据f(n,i)与k的关系就能确定k所在的子树,从而避免搜索无用子树。找到了目标子树也就确定了第i位元素。
如果
f
(
n
,
i
)
>
k
f(n,i)>k
f(n,i)>k,说明k在第一棵子树中;
如果
f
(
n
,
i
)
<
k
<
2
f
(
n
,
i
)
f(n,i)<k<2f(n,i)
f(n,i)<k<2f(n,i),说明k不在第一棵子树,而在第二棵子树的第
k
−
f
(
n
,
i
)
k-f(n,i)
k−f(n,i)个叶子节点;
…;
如果
(
n
−
i
)
∗
f
(
n
,
i
)
<
k
<
(
n
−
i
+
1
)
∗
f
(
n
,
i
)
(n-i)*f(n,i)<k<(n-i+1)*f(n,i)
(n−i)∗f(n,i)<k<(n−i+1)∗f(n,i),说明k在第n-i+1棵子树中的第
k
−
(
n
−
i
)
∗
f
(
n
,
i
)
k - (n-i)*f(n,i)
k−(n−i)∗f(n,i)个叶子节点;
如果
k
>
(
n
−
i
+
1
)
∗
f
(
n
,
i
)
k>(n-i+1)*f(n,i)
k>(n−i+1)∗f(n,i)说明不存在第k个叶子节点。
实际编码中,我们使用辗转相减法更新k的值,若k<f(n,i),则k -= f(n,i)。
确定了是第
m
(
1
<
m
≤
n
−
i
+
1
)
m(1<m\leq n-i+1)
m(1<m≤n−i+1)棵子树,也就确定了第i位元素是可选元素中字典序为m的元素,那么接下来就是深搜确定第i+1位的元素。如此递归直到找到叶子节点,即为所求。
如何快速找到可选元素中字典序为
m的元素?
如果我们在确定当前元素后,都能保证可选元素保持字典序,那么可选元素中字典序为m的元素就是
nums[i+m]。
办法就是使用依次交换的方式确定当前位置的元素。例如确定第一位元素:
[1,2,3,4],如果1不是目标元素,就交换1,2:[2,1,3,4]
如果2不是目标元素,就交换2,3:[3,1,2,4]
如果3不是目标元素,就交换3,4:[4,1,2,3]
可以发现在确定第一位元素之后,后面3位元素仍然保持字典序。
public class Solution {
private List<List<Integer>> permutations = new ArrayList<>();
private void swap(int[] nums, int i, int j) {
nums[i] = nums[i] ^ nums[j] ^ (nums[j] = nums[i]);
}
private int[] factorial = new int[11];
private void calculateFactorial(int n) {
factorial[1] = 1;
for (int i = 2; i <= n; i++) {
factorial[i] = factorial[i - 1] * i;
}
}
/**
* 判断是否有第k个排列,若有返回true,否则返回false
*/
private boolean kthPermutation(int[] nums, int lo, int hi, int k) {
if (k == 1) { return true;}
boolean flag = false;
for (int i = lo; i < hi; i++) { // 当前有hi - lo棵子树
int res = hi - lo - 1; // 当前子树有factorial[res]个叶子节点
if (factorial[res] >= k) // 第k个叶子节点在当前子树中
flag = kthPermutation(nums, lo + 1, hi, k);
else if (factorial[res] < k) {// 如果factorial[res] < k,说明k不在当前子树
if (i + 1 == hi) break; // k不在最后一棵子树中
swap(nums, lo, i + 1); // 交换nums[lo]与字典序为i+1的可选元素
k -= factorial[res]; // 来到下一棵子树。
}
if (flag) return true;
}
return false;
}
public String getPermutation(int n, int k) {
int[] nums = new int[]{1,1,2,2};
// for (int i = 0; i < n; i++) {nums[i] = i + 1;}
calculateFactorial(n);
System.out.println(kthPermutation(nums, 0, n, k));
StringBuilder ans = new StringBuilder();
for (int i = 0; i < n; i++) { ans.append(nums[i]); }
return ans.toString();
}
}
448

被折叠的 条评论
为什么被折叠?



