LeetCode 60. Permutation Sequence

一 题目

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:

  1. "123"
  2. "132"
  3. "213"
  4. "231"
  5. "312"
  6. "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

https://soulmachine.gitbooks.io/algorithm-essentials/java/linear-list/array/permutation-sequence.html

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值