一 题目
The set [1,2,3,...,n]
contains a total of n! unique permutations.
By listing and labeling all of the permutations in order, we get the following sequence for n = 3:
"123"
"132"
"213"
"231"
"312"
"321"
Given n and k, return the kth permutation sequence.
Note:
- Given n will be between 1 and 9 inclusive.
- Given k will be between 1 and n! inclusive.
Example 1:
Input: n = 3, k = 3
Output: "213"
Example 2:
Input: n = 4, k = 9
Output: "2314"
二 分析
求n个数字的第k个排列组合,medium级别难度。
之前做过 leetcode 31. Next Permutation ,所以即调用k-1次nextpermutation(),从而得到第k个排列。这个方法把前k个排列全部求出来了,时间复杂度是O(kn)。
public String getPermutation(int n, int k) {
int[] nums =new int[n];
for(int i=0;i<n;i++){
nums[i]= i+1;
}
for(int j=1;j<k;j++){
nextPermutation(nums);
}
String res ="";
for(int num:nums){
res =res+num;
}
return res;
}
Runtime: 23 ms, faster than 17.86% of Java online submissions for Permutation Sequence.
Memory Usage: 34.3 MB, less than 100.00% of Java online submissions forPermutation Sequence.
显然这不是好办法,浪费很多。有没有直接求出第K个排列呢?有,康托展开式。
2.1 康托展开
大神就可以略过去了,我看了算法珠玑的介绍,没看懂,又看了wiki的介绍,以下引自wiki:
康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。
以下称第x个全排列是都是指由小到大的顺序。
ai 表示原数的第i位在当前未出现的元素中是排在第几个
康托展开的逆运算
既然康托展开是一个双射,那么一定可以通过康托展开值求出原排列,即可以求出n的全排列中第x大排列。
康托展开与逆展开是将全排列和它的字典序互相转化的两种算法.知乎上有详细的论证过程:https://zhuanlan.zhihu.com/p/39377593
康托展开举例:
例如,3 5 7 4 1 2 9 6 8 展开为 98884。因为X=2*8!+3*7!+4*6!+2*5!+0*4!+0*3!+2*2!+0*1!+0*0!=98884.
解释:
排列的第一位是3,比3小的数有两个,以这样的数开始的排列有8!个,因此第一项为2*8!
排列的第二位是5,比5小的数有1、2、3、4,由于3已经出现,因此共有3个比5小的数,这样的排列有7!个,因此第二项为3*7!
以此类推,直至0*0!
康托展开逆运算举例:
既然康托展开是一个双射,那么一定可以通过康托展开值求出原排列,即可以求出n的全排列中第x大排列。
如n=5,x=96时:
首先用96-1得到95,说明x之前有95个排列.(将此数本身减去1) 用95去除4! 得到3余23,说明有3个数比第1位小,所以第一位是4. 用23去除3! 得到3余5,说明有3个数比第2位小,所以是4,但是4已出现过,因此是5. 用5去除2!得到2余1,类似地,这一位是3. 用1去除1!得到1余0,这一位是2. 最后一位只能是1. 所以这个数是45321.
理解了上面的公式,代码就简单了,关键的就几行。
public static void main(String[] args) {
// TODO Auto-generated method stub
String res = getPermutation(4, 9);
System.out.println(res);
}
public static String getPermutation(int n, int k) {
// 初始化:使用链表
ArrayList<Integer> numberList = new ArrayList<Integer>();
for (int i = 1; i <= n; i++) {
numberList.add(i);
}
//K 索引-1
k--;
// 求 n的阶乘
int[] f= new int[n];
f[0] = 1;
for (int i = 1; i < n; i++) {
f[i] =i*f[i-1];
}
String result = "";
// 计算排列
for (int i = n; i >0; i--) {
//根据规律计算索引的位置
int curIndex = k / f[i-1];
// 更新 k
k = k % f[i-1];
// 根据索引获取数字
result += numberList.get(curIndex);
// 删除已获取的数字
numberList.remove(curIndex);
}
return result.toString();
}
网上很多代码都是C或者C++的,简洁了很多。java 有的使用了数组的方式保存结果,因为数组不方便删除,使用了bool类型的数组visited来标记数组那个元素已经出现过了。个人觉得不如list简洁,原理是一样的。
Runtime: 1 ms, faster than 99.45% of Java online submissions for Permutation Sequence.
Memory Usage: 34.5 MB, less than 100.00% of Java online submissions forPermutation Sequence.
时间复杂度是O(N),可见康托展开式是针对全排列的最优解。
在看个康托展开的的例子,
X=a1(n−1)!+a2(n−2)!+⋯+an⋅0!
上面的题目的例子,2314对应的全排列是第几位?
public static void main(String[] args) {
// TODO Auto-generated method stub
int res = kangtuo("2314");
System.out.println(res);
}
/**
* 求解康拓展开:
* @param n
* @param seq
* @return
*/
public static int kangtuo(String seq){
//因为1234这样的升序默认为全排列的第一个
int res =1;
int n = seq.length();
// 求 n的阶乘
int[] f= new int[n];
f[0] = 1;
for (int i = 1; i < n; i++) {
f[i] =i*f[i-1];
}
for(int i=0;i<n;i++){
int cnt = 0;
for(int j=i+1;j<n;j++){
// 计算seq[i]有几个比它小的数
if(seq.charAt(i)>seq.charAt(j)){
cnt++;
}
}//按规律乘以对应的阶乘
res += cnt*f[n-i-1];
}
return res;
}
参考:
https://zh.wikipedia.org/wiki/%E5%BA%B7%E6%89%98%E5%B1%95%E5%BC%80