奇妙的非递归算法取全排列

来源:互联网 发布:手机知乎如何匿名提问 编辑:程序博客网 时间:2024/06/08 12:45

下午偶尔翻到以前的写到的一些代码,不觉心血来潮,看了一下其中关于全排列算法的一个实现.联想到高中时数学课上学到关于组合和排列的一些公式,于是在纸上涂鸦着一些计算方法,突然灵感潮来,想借助小白鼠的那个思路也能将全排列解决出来~

 

下班回到家便赶紧吃饭玩一局sc后,开始实现,终于赶在睡觉之前可以写这篇blog,介绍我的这种还算奇妙的全排列算法:
1. 算法思路:
           试想N个自然数A1,A2,...,AN的全排列是N!个,那么,对于每种排列,我将其打印出来,遍历一遍至少是O(N!)的复杂度,那么能不能在就在这个复杂度内用非递归解决这个问题呢?我的想法就是从这点开始成形.同时借助了建模的思想分析的.
           无论如何,我首先选取一个自然数 A1 出来,放到存放地址的中间, 那么在A1的周围存在两个位置,一个是A1的左边,一个是A1的右边,那么,我抽出另一自然数出来(假设是AK),便有两种存放的可能性:
(AK,A1) , (A1,AK).
           如果我将1周围的两个位置用0和1来表示,如果放在左边,那么左边用1表示,反之用0表示.反正只能放一个地方,那么这两个位置合起来不是10,就是01,化成十进制便是2或者1.
           同理,如果第第二个数A2放在A1的左边,也就是第一轮获取了2,排列就是(A2,A1),当第三个数(假设是A3)到来时,那么它就存在三个位置可放了,分别是A2的左边,A2和A1之间,A1的右边,同样用0和1标志每个位置,如果A3放在A2左边,那么构成100(十进制4),如果中间则构成010(十进制2),A1的右边001(十进制1),那么可以用第二个值来表征第三个数摆放的位置.
           如此,细心的玩家已经大概想明白了我要干嘛了,对,如此以往,可以将全部N个自然数用N-1个数值来替代(这些数字是有规律的,2的次方),接下来,我想到的便是,一定要找到这些数字和它在全排列中一一对应的关系,那么问题便解决了.
           我们拿3个自然数的例子来分析,3!=6,对于3个自然数的全排列我将其编号1-6组,每组按上面的规则应该可以唯一对应一个序列,如将第一组对应于(1,1),  是按上面规则来的.  第二组对应于(1,2), 第三组对应于(2,1), 第四组对应于(2,2), 第五组对应于(4,1), 第六组对应于(4,2). 于是, 我只要分析出一个计算规则便可以只要遍历这N!个分组,便可以求出每个分组对应的排列了.
           很显然, 由P(M,M)的计算规则,P(M,M)=M*(M-1)*...*1可以看出,如果我在第2个到来时,将m = P(M,M)%2 同时,P=P/2,那么,可以很好的处理第二个到来时所做的寻址选择:如果m是0,那么就放右边,如果m是1,就放左边,而对应于第三个到来时,同样如此,那么这个存放规则便生成了.于是遍历N!次,按辗转法则,便可以直接生成每个组对应的全排列了.
           应该明白了吧?

2.算法实现(C语言版):
           结合上面的思路,编写代码,其中逻辑还是挺麻烦的.
 

引用
[yhzh@localhost test]$ cat test_permute1.c
#include < stdio.h>
#include < stdlib.h>

#define N 10                 //偷懒,定义个宏吧.用数组省事
int permute(int perm);

int main(int argc,char *argv[])
{
       int ret = 0;

       ret = permute(N);

       return ret;
}

int permute(int perm)
{
       char i = 1;
       int j = 1;
       int k = 1;
       int ptr[2*N] = {0};         //其实只需要N的空间即可,未来偷懒,定义2N的数组取代队列,这也是数组替代队列的典例

       while((i <= perm) && (j = j*i))   //这个while和下面第一个for的意思是遍历N!个数字,从1到N!,添加print语句试试
       {
               for(;k <= j;k++)
               {
                       int begin = N;    //队列的头位置(处于2N数组中的位置)
                       int end = N;       //队列的尾位置(处于数组中的位置),后面需要注意end在左边,begin在右边
                       int l = 1;             //代排列的自然数,偷懒,就用1到N吧.方便,可以++就行了
                       int n = k;            //辗转法则之基数
                       int m = 0;          //辗转法则之分离数
                       int t = 1;            //控制生成1到N个自然数的一个和K对应的排列

                       ptr[N] = l;          //将数组的N位置设置为队列的头和尾所在地.同时放入第一数:1

                       while(t < perm)   //生成和K对应队列,一个一个放入2到N
                       {
                               t++;
                               l++;

                               m = n%t+1;     //辗转,+1为什么?呵呵,测试一下吧(注意求余可是有余数0啊)
                               n = n/t;

                               if( m == 1)         //如果m==1,意味着n正好被t整除,那么意味着下一个数是永远放在最右边一个数的右边,因此begin++了.end不动.
                               {
                                       ptr[++begin] = l;
                               }
                               else                 //放左边了.至于是放在左边过去的第几个位置,有m的值而定,因此会有move = begin - (m -1),因此从end到该move位置之间的所有数据要向后移动.因此会有下面的for语句,如果用队列,则不需要,数组模拟队列就是有些麻烦
                               {
                                       int move = 0;

                                       move = begin - (m - 1);          //确定放置位置
                                       if(move < end)                         //过end了,其实也只是过去一位,可以在这里assert一下,那么直接加.等于是在队列末尾添加数字.
                                       {
                                               ptr[move] = l;
                                               end = move;
                                       }
                                       else                                          //在队列中间加入数字
                                       {
                                               int index = end;

                                               for(;index<=move;index++)
                                               {
                                                       ptr[index-1] = ptr[index];
                                               }
                                               ptr[move] = l;
                                               end = end - 1;
                                       }
                               }
                       }//while t                                             //循环结束

                       int tmp;
                       for(tmp=end;tmp<=begin;tmp++)         //打印结果
                               printf("%d ",ptr[tmp]);
                       printf("/n");
               }
               i++;
       }

       return 0;
}



           将宏N的值设置成适当大小,编译之后,运行试试.当然还是要惦量一下自己的机器.在我的最小配置dell latitude D620上测试的结果如下:
           对于求解10个的全排列:
 

引用

[yhzh@localhost test]$ time ./test_permute1  //最新的代码,非递归
.........
10 9 8 7 6 5 2 4 3 1
10 9 8 7 6 5 3 4 1 2
10 9 8 7 6 5 3 4 2 1
10 9 8 7 6 5 4 1 2 3
10 9 8 7 6 5 4 2 1 3
10 9 8 7 6 5 4 1 3 2
10 9 8 7 6 5 4 2 3 1
10 9 8 7 6 5 4 3 1 2
10 9 8 7 6 5 4 3 2 1
1 2 3 4 5 6 7 8 9 10

real    1m21.360s
user    0m6.880s
sys     0m12.921s
[yhzh@localhost test]$ time ./test_permute1  //去掉输出
real    0m1.455s
user    0m1.448s
sys     0m0.000s
 


           同时,我也运行了下我很早写的那段代码:
 

引用

[yhzh@localhost test]$ time ./test_permute2   //我以前写的代码,递归
.......
10  9  8  7  6  5  3  4  1  2
10  9  8  7  6  5  3  4  2  1
10  9  8  7  6  5  4  1  2  3
10  9  8  7  6  5  4  1  3  2
10  9  8  7  6  5  4  2  1  3
10  9  8  7  6  5  4  2  3  1
10  9  8  7  6  5  4  3  1  2
10  9  8  7  6  5  4  3  2  1
       @3628800@
real    1m39.231s
user    0m5.220s
sys     0m14.889s
[yhzh@localhost test]$ time ./test_permute2  //去掉输出
real    0m0.707s
user    0m0.696s
sys     0m0.000s
 


           还在网上找了段评价很不错的代码编译运行了试试:
 

引用

[yhzh@localhost test]$ time ./test_permute3  //网上一个朋友的代码,递归
..........
 9 10  7  8  6  5  4  3  2  1
10  9  7  8  6  5  4  3  2  1
 8  9 10  7  6  5  4  3  2  1
 9  8 10  7  6  5  4  3  2  1
 8 10  9  7  6  5  4  3  2  1
10  8  9  7  6  5  4  3  2  1
 9 10  8  7  6  5  4  3  2  1
10  9  8  7  6  5  4  3  2  1
real    1m41.827s
user    0m6.828s
sys     0m14.765s
[yhzh@localhost test]$ time ./test_permute3  //去掉输出
real    0m1.044s
user    0m0.224s
sys     0m0.000s
 


           还给出一段读起来很心旷神怡的代码:
 

引用

[yhzh@localhost test]$ time ./test_permute4  //呵呵,这段代码值得推敲
real    0m0.219s
user    0m0.196s
sys     0m0.004s
 


   列出test_permute2.c和test_permute3.c还有test_permute4.c:
 

引用
test_permute2.c
#include < stdio.h>

#define N 10                                        /* N */
#define K 10                                                /* K */

int a[K];                                                /* 定义数组来存储中间结果 */
int b[K];                                                /* 定义数组来存储结果 */
int flag[K];                                        /* 定义一个标志数组 */
int num;                                                /* 计算P(N~K)的值 */

void print()                                        /* 打印函数 */
{
      int i;
           
      //for (i=0;i < K;i++)
        //      ;//printf("%3d",b[i]);                /* 打印出结果 */
    //  printf("/n");
   
      return;
      }
   
void init_abflag()
{        int i;
   
      num=0;
      for (i=0;i < K;i++)
      {        a[i]=0;                                        /* a初始化为0 */
              b[i]=0;                                        /* b初始化为0 */
              flag[i]=0;                                /* flag初始化为0 */
              }
      return;      
      }

void perm(int k)                                /* 对每个组合的结果求全排列 */
{        int i;
   
      if (k==K)                                        /* 输出条件 */
      {        num++;                                        /* 每输出一次即是一个排列 */
              print();
              return;
              }
      for (i=0;i < N;i++)
      {        if (flag[i] == 0)                /* 这一个数值有没有用过 */
              {        flag[i]++;                        /* 没有则用 */
                      b[k]=a[i];                        /* 即把a中的值送给b */
                      perm(k+1);                        /* 进入递归 */
                      flag[i]--;                        /* 复原 */
                      }
              }
   
      return;
      }

void con(int n,int k)
{        int i;
/*        printf(" !%d! ",k);*/
   
      if (k==0)                                        /* 求组合,k=0,作为第一个成功返回的条件 */
      {        perm(0);                                /* 对每个组合调用全排列 */
              return;
              }
           
      if (n==k)                                        /* 求组合,k=n作为第二个成功返回的条件 */
      {        for(i=0;i < K;i++)
                      a[i]=i+1;                        /* 把后面的全输出 */
              perm(0);                                /* 对每个组合调用全排列 */
              return;              
              }
                   
      for (i=n;i>k-1;i--)                        /* 程序的核心部分 */
      {        a[k-1]=i;                                /* 送入第一个数据 */
              con(i-1,k-1);                        /* 进入递归 */
              }

      return;
      }

int main()
{        init_abflag();                                /* 初始化数组 */
           
      con(N,K);                                        /* 求解 */
      //printf("/t@%d@",num);                /* 打印出总个数 */
      return 0;      
      }
test_permute3.c
#include < stdio.h>
#include < stdlib.h>
void put(int *);

int N;
int main()
{
int i,j,k,m,n,*l,*a,m2;
N = 10;
/* 开数组 */
l=(int *)malloc(N*sizeof(int));
a=(int *)malloc(N*sizeof(int));
/* 赋初值 */
for(i=0;i{
a[i]=0;l[i]=i+1; /* a[i]-1是第i个元素交换的次数 */
}
m=2;put(l);
do
{
n=l[1];l[1]=l[0];l[0]=n;put(l); /* 头两个先换一下 */
/* 判断该换哪个了 */
while(a[m-2] == m)
m++;
if(m==N) break;
/* 下面是整理过程,可能还有办法 */
n=m>>1;
 for(i=0,j=m-1;i{
k=l[i];
l[i]=l[j];
l[j]=k;
} /* 以上是从小到大重排,下面处理好了可能不必做,因为可以无序
*/
m2=m-2;
a[m2]++;
/* 下面该把第j个和第m个交换 */
j=m-a[m2];
n=l[j];l[j]=l[m];l[m]=n;
put(l);
/* m以前的重排 */
for(i=0;ia[i]=0;
m=2;
} while(1);
}
void put(int *l)
{
int i;
for(i=0;i//printf(" %2d",l[i]);
//putchar('/n');
}

test_permute4.c
#include

void permutation(char a[], int m, int n) {   // 排列算法
   int i;
   char t;
   if (mpermutation(a, m+1, n);
for (i=m+1;it=a[m]; a[m]=a[i]; a[i]=t;
permutation(a, m+1, n);
t=a[m]; a[m]=a[i]; a[i]=t;
}
} else {
//printf("%s/n", a); //屏蔽掉输出
}
}

int main() {
char a[]="ABCDEFGHIJ";
permutation(a, 0, 10);
return 0;
}
 


全排列有如此奇妙的方法,那么组合呢?如果组合可以类似解决,那么排列也就可以由全派了和组合复合求解了.

 

原创粉丝点击