写了几个关于全排列的东西,然后就接触到了康拓排列。之前对于全排列的非递归算法耿耿于怀,一只不能找到好的方式。现在好了,有了康拓,什么都解决了。
递归求全排列
我们先来看一个简单的例子,就是如何递归的求全排列。
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;
}