康拓排列的自我總結--以及全排列的遞歸非遞歸算法

寫了幾個關於全排列的東西,然後就接觸到了康拓排列。之前對於全排列的非遞歸算法耿耿於懷,一隻不能找到好的方式。現在好了,有了康拓,什麼都解決了。

遞歸求全排列

我們先來看一個簡單的例子,就是如何遞歸的求全排列。

private static void recursionPermutation(String[] s, int k) {
		// TODO Auto-generated method stub
		if (k == s.length - 1) {
			for (int i = 0; i < s.length; i++) {
				System.out.print(s[i]+" ");
			}
			System.out.println();
		}
		for(int i=k;i<s.length;i++){
			swap(s,i,k);
			recursionPermutation(s, k+1);
			swap(s,k,i);
		}
	}
上述代碼對String數組s進行全排列,當然你也可以對int數組全排列, 換一下就好了,無所謂的。採用的是遞歸的方式,k相當於一個標杆。對這個標杆和其他元素交換,然後遞歸的調用,然後恢復原來的狀態。在把標杆k和其他的交換。遞歸的結束條件是k等於最後一個了,說明已經前面的都當過標杆了,都交換過了,所以要輸出。


上述就是求全排列的遞歸算法了,那麼這裏引入一個概念,就是康拓排列,什麼是康拓排列呢?一個數組,求它的全排列,然後按照從小到大的順序(這個可以是字典順序啊,或者你定義的其他順序都行)排好,那麼這就是康拓排列。比如{1,2,3},它的康拓排列就是{1,2,3},{1,3,2},{2,1,3},{2,3,1},{3,1,2},{3,2,1}。

康拓排列有一個前提是裏面的元素不能有重複的!

求下一個排列

這是一個經典問題,前面已經減了什麼事康拓排列了,那麼給你一個排列,讓你求它的下一個排列是什麼,這個算法應該怎麼寫呢?比如上述例子,{2,1,3}的下一個排列是{2,3,1}。

這個例子的算法如下,不多說,自己慢慢看。
public class NextPermutation {

	//分析,從右到左,找到升序排列的隊列A,然後再往左一個,這個定義爲PartitionNumber
	//然後再從右到左,找到比PartitionNumber大的第一個數字,定位ChangeNumber
	//交換這兩個數字
	//逆序交換後的隊列A
	public static void main(String[] args){
		int[] nums ={1,2,4,3};
		int[] next = NextPermutation(nums);
		System.out.println(Arrays.toString(next));
	}

	public static int[] NextPermutation(int[] nums) {
		// TODO Auto-generated method stub
		
		//找到從右往左升序的隊列,並找到PationNumber;
		int pivot = nums.length-1;
		while(pivot>0&&nums[pivot]<nums[pivot-1]){
			pivot--;
		}
		int partitionNumberSite = pivot-1;
		int partitionNumber = nums[partitionNumberSite];
		//找到比partitionNumber大的第一個數字,即changeNumber的位置;
		int i;
		for(i=nums.length-1;i>=pivot;i--){
			if(nums[i]>partitionNumber){
				break;
			}
		}
		//交換兩個元素
		swap(nums,i,partitionNumberSite);
		//逆序排序
		for(int j=pivot,k=nums.length-1;j<k;j++,k--){
			swap(nums, j, k);
		}
		return nums;
	}

	private static void swap(int[] nums, int i, int j) {
		// TODO Auto-generated method stub
		int temp = nums[i];
		nums[i]=nums[j];
		nums[j]=temp;
	}
}
有了求下一個排列,那麼你的康拓全排列是不是就很簡答就能求出來了呢?只需要連續調用n!次nextPermutation方法,就可以非遞歸的求得所有的全排列。

判斷一個排列的位置

康拓排列有一個典型的應用就是判斷一個排列的位置,給你一個排列){1,2,3},那麼這個排列肯定是第一個啊。那麼對於四個數字的呢?5個數字的{1,3,5,2,4},它有在哪一個呢?
這個的算法的思想如下:
先了解什麼是康拓展開
形如下式的炸開叫做康拓展開,
X=an*(n-1)!+an-1*(n-2)!+...+ai*(i-1)!+...+a2*1!+a1*0!
ai爲整數,並且0<=ai<i(1<=i<=n),其中ai爲小於上一個數字的個數。

適用範圍:沒有重複元素的全排列

{1,2,3,4,...,n}的排列總共有n!種,將它們從小到大排序,怎樣知道其中一種排列是有序序列中的第幾個?

如 {1,2,3} 按從小到大排列一共6個:123 132 213 231 312 321。想知道321是{1,2,3}中第幾個大的數。
於是我們建立3個桶,我們知道3個桶裏面已經放好數字了,分別是3,2,1.現在我們想知道這個數字是第幾大的。怎麼辦呢,我們看第一個桶,第一個桶中的數字是3,但是第一個桶我們知道如果你來放的話,是可以放1,2,3三個數字的,那麼這裏面比三小的數字有1,和 2連個,於是,由1或者2開頭的全排列都比3開頭的全排列要小(100多肯定小於300多啊),於是我們知道由1開頭的有1*2!(階乘)個,由2開頭的有1*2!(階乘)個,所以,這裏有2*2!個。再看第二個桶,同理,第二個桶被放了2,此時第一個桶已經放好3了,所以第二個桶你來放的話,只能放2和1。我們知道,32*肯定比31*要大,而滿足這個條件的,只有1*1!(階乘)種。最後第三個桶,已經沒有比它小的了,所以只能乘以0了。於是就是2*2!+1*1!+0*0!=5.所以這個排列是第6大的(別忘記加1)
再舉個例子:1324是{1,2,3,4}排列數中第幾個大的數:第一位是1小於1的數沒有,是0個,0*3!,第二位是3小於3的數有1和2,但1已經在第一位了,所以只有一個數2,1*2! 。第三位是2小於2的數是1,但1在第一位,所以有0個數,0*1!,所以比1324小的排列有0*3!+1*2!+0*1!=2個,1324是第三個大數。
代碼非常簡單,如下
public static int cantor(int[] nums){
		int result =0;
		int length = nums.length;
		for(int i=0;i<length;i++){
			int temp=0;
			for(int j=i+1;j<length;j++){
				if(nums[i]>nums[j]){
					++temp;
				}
			}
			result+=temp*factorial(length-i-1);
		}
		return result+1;
	}

給你一個數組,求第k個排列。

這個題目有兩種做法,一種是執行k-1次nextPermutation,另外一種就是使用康託排列了。重點講康託排列。
如何找出第16個(按字典序的){1,2,3,4,5}的全排列?

1. 首先用16-1得到15

2. 用15去除4! 得到0餘15

3. 用15去除3! 得到2餘3

4. 用3去除2! 得到1餘1

5. 用1去除1! 得到1餘0

有0個數比它小的數是1,所以第一位是1

有2個數比它小的數是3,但1已經在之前出現過了所以是4

有1個數比它小的數是2,但1已經在之前出現過了所以是3

有1個數比它小的數是2,但1,3,4都出現過了所以是5

最後一個數只能是2

所以排列爲1 4 3 5 2

下面是兩種方法的代碼:
private static void getPermutationByNext(int i, int j) {
		// TODO Auto-generated method stub
		//初始化數組的值爲1,2,3,......i;
		int[] nums = new int[i];
		for(int a=0;a<nums.length;a++){
			nums[a]=a+1;
		}
		//因爲第1個是nums,所以只需要再求j-1次即可
		for(int a=1;a<j;a++){
			nums = NextPermutation.NextPermutation(nums);
		}
		System.out.println(Arrays.toString(nums));
		
	}
//給定一個數字n,表示從1--n的數組,然後找第k個排列
	public static int[] reverseCantor(int n,int k){
		int[] result = new int[n];
		//用list,當這個元素添加到result數組時,刪除這個元素
		ArrayList<Integer> list = new ArrayList<>();
		
		for(int i=0;i<n;i++){
			list.add(i+1);
		}
		//求一開始的階乘(n-1)!
		int base=factorial(n-1);
		//別忘記減一
		int cantor = k-1;
		//操作放在for裏面
		for(int i=n-1;i>0;cantor%=base,base/=i,i--){
			//求得結果,這個結果代表着第a小的值;
			int a=cantor/base;
			result[n-i-1]=list.get(a);
			list.remove(a);
		}
		//別忘記最後一個元素
		result[n-1]=list.get(0);
		
		return result;
	}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章