全排列算法

我们知道,对于N个字符的全排列之间存在一定的关系,比如,1234,之后的后继排列为1243(采用字典排序法)。一般对任何一种排列算法,除最后一个排列以外,所有排列都会有一后续排列。排列算法就是确定如何从已知的一个排列计算下一个(后续)排列。

常见的排列算法有:

  1. 字典排序法
  2. 递增进位制数法
  3. 递减进位制数法
  4. 邻位对换法
  5. 递归法
定义用于交换和输出数组的函数:
void swap(int * x,int * y){
     int t=*x;
     *x=*y;
     *y=t;
}
void output(int array[],int n){
    int i=0;
    for(i;i<n;i++)
        printf("%d-",array[i]);
    printf("\n");
}


一,字典排序法

对给定的字符集中的字符规定一个先后关系,然后在此基础上按照顺序依次产生每个排列。
例如,假设有字符集,{1,2,3}。其所确定的关系就是:1<2<3。这样按照字典排序的方法所生成的全排列是:123,132,213,231,312,321。算法思想如下:
  
设P是[1,n]的一个全排列,P=P1P2…Pn=P1P2…Pj-1PjPj+1…Pk-1PkPk+1…Pn,过程如下:
1)计算  j=max{i|Pi<Pi+1}。就是从最右边开始找第一个比右边小的数字,假定这个数字是第j个。
2)计算  k=max{i|Pi>Pj} ,从最右边开始找,第一个比上一步中的Pj大的数字,设为Pk。
3)对换Pj,Pk,并将Pj+1…Pk-1PjPk+1…Pn翻转, P’= P1P2…Pj-1PkPn…Pk+1PjPk-1…Pj+1即P的下一个,这样就由已知的排列P得到下一个排列P'。

int dict_arrange(int a[],int n){
        int i,j;
        output(a,n); // 输出最初的串
        i=n-2; // 从倒数第二个数字开始与其后继数字进行比较
        while(i>=0){
                if (a[i]<a[i+1]){  // 找到一个比右边小的数字a[i]。
                        for(j=n-1;j>=i+1;j--){ // 再从右边开始,找到第一个比a[i]大的数字a[j]。
                                if(a[i]<=a[j])
                                break;
                        }
                        swap(&a[i],&a[j]); // 交换a[j]和a[i]
                        for(j=n-1;j>=0;j--){ // 将i以后的数字(从i+1开始,至n-1结束)倒置。
                                i+=1;
                                if (i>=j) break;
                                swap(&a[i],&a[j]);
                        }
                        output(a,n); // 输出新的排列。
                        i=n-1; // 重新开始下一趟,就是又要从末尾开始找。
                }
                i-=1;
        }
}
二,递增进位制数法
在递增进位制数法中,从一个排列求另一个排列需要用到中介数。如果用 ki表示排列p1p2...pi...pn中元素pi的右边比pi小的数的个数,则排列的中介数就是对应的排列k1 ...... ki...... kn-1。
  例如排列839647521的中介数是72642321,7、2、6、......分别是排列中数字8、3、9、......的右边比它小的数字个数。
  中介数是计算排列的中间环节。已知一个排列,要求下一个排列,首先确定其中介数,一个排列的后继,其中介数是原排列中介数加1,需要注意的是,如果中介数的末位kn-1+1=2,则要向前进位,一般情形,如果ki+1=n-i+1,则要进位,这就是所谓的递增进位制。例如排列839647521的中介数是72642321,则下一个排列的中介数是67342221+1=67342300(因为1+1=2,所以向前进位,2+1=3,又发生进位,所以下一个中介数是67342300)。
  得到中介数后,可根据它还原对应得排列。
  算法如下:
  中介数k1、k2、......、kn-1的各位数字顺序表示排列中的数字n、n-1、......、2在排列中距右端的的空位数,因此,要按k1、k2、......、kn-1的值从右向左确定n、n-1、......、2的位置,并逐个放置在排列中:i放在右起的ki+1位,如果某位已放有数字,则该位置不算在内,最后一个空位放1。
  因此从67342300可得到排列849617523,它就是839647521的后一个排列。因为9最先放置,k1=6,9放在右起第7位,空出6个空位,然后是放8,k2=7,8放在右起第8位,但9占用一位,故8应放在右起第9位,余类推。

三,递减进位制数法
在递增进位制数法中,中介数的最低位是逢2进1,进位频繁,这是一个缺点。把递增进位制数翻转,就得到递减进位制数。
  839647521的中介数是67342221(k1k2...kn-1),倒转成为12224376(kn-1...k2k1),这是递减进位制数的中介数:ki(i=n-1,n-2,...,2)位逢i向ki-1位进1。给定排列p,p的下一个排列的中介数定义为p的中介数加1。例如p=839647521,p的中介数为12224376,p的下一个排列的中介数为12224376+1=12224377,由此得到p的下一个排列为893647521。
  给定中介数,可用与递增进位制数法类似的方法还原出排列。但在递减进位制数中,可以不先计算中介数就直接从一个排列求出下一个排列。具体算法如下:
  1)如果p(i)=n且i<>n,则p(i)与p(i-1)交换
  2)如果p(n)=n,则找出一个连续递减序列9、8、......、i,将其从排列左端删除,再以相反顺序加在排列右端,然后将i-1与左边的数字交换
  例如p=893647521的下一个排列是983647521。求983647521的下一个排列时,因为9在最左边且第2位为8,第3位不是7,所以将8和9从小到大排于最右端364752189,再将7与其左方数字对调得到983647521的下一个排列是367452189。又例如求987635421的下一个排列,只需要将9876从小到大排到最右端并将5与其左方数字3对调,得到534216789

四,邻位对换法
邻位对换法中下一个排列总是上一个排列某相邻两位对换得到的。以4个元素的排列为例,将最后的元素4逐次与前面的元素交换,可以生成4个新排列:
  1 2 3 4 1 2 4 3 1 4 2 3 4 1 2 3
  然后将最后一个排列的末尾的两个元素交换,再逐次将排头的4与其后的元素交换,又生成四个新排列:
  4 1 3 2 1 4 3 2 1 3 4 2 1 3 2 4
  再将最后一个排列的末尾的两个元素交换,将4从后往前移:
  3 1 2 4 3 1 4 2 3 4 1 2 4 3 1 2
  如此循环既可求出全部排列。

五,递归

对于全排列,用递归实现是比较简洁的,但是当数据很多的时候,明显不合适。
void perm(int a[],int k,int n){ // 当前递归第k层,在执行时应从n开始.
     int i;
     if(k==1){
         for(i=0;i<n;i++)
              printf("%d-",a[i]);
     }else{
         for(i=n-k;i<n;i++){
              swap(a[i],a[n-k]);
              perm(a,k-1,n);
              swap(a[i],a[n-k]);
         }
     }
}
上面递归在具体调用时,如
int main(int argc,char ** argv){
        int a[] = {3,2,4,1};
        int n = 4;
        perm(a,n,n);
        return 0;
}

你可能感兴趣的:(全排列算法)