打印全排列思路


長篇慎入,點擊直接跳看最后一段代碼

從n個不同的元素中取m個元素(m<=n),按照一定的順序排列起來,

叫做從n個不同元素取出m個元素的一個排列。

當m=n時,所有的排列情況叫做全排列,比如3的全排列為:

1 2 3
1 3 2

2 1 3
2 3 1

3 1 2
3 2 1


我們先從簡單的開始,要求寫出代碼打印上面的排列情況即可,順序可以不一致。

分析過程:

首先,我們如何把三位的數字打印出來呢,有兩種方式:

printf("%d\n" ,num);  //num=123

第二種:

printf("%d%d%d\n",a,b,c); //a=1 b=2 c=3

我認為采用第二種比較好,原因在於第一種需要對位數的考慮,

而我們問題需要對數字位置不斷的進行交換,因此第二種也方便於交換。

 

現在知道了如何打印,那么如何交換數字的位置呢?很簡單:

void swap(int *a,int *b){
  int tmp = *a;
  *a = *b;
  *b = tmp;
}

 

於是將上面的交換打印結合起來就可以,大概寫個模型了

void output(int a,int b,int c){
   printf("%d%d%d\n",a,b,c);
   swap(&a,&b);
output(a,b,c); //遞歸 }

 

如果此時執行 output(1,2,3),那么程序將會死循環打印出下面的片段

123
213
..

 

因此我們有兩個問題要解決:

1.怎樣交換使得所有全排列都能被遍歷到?

2.怎樣使得遞歸能夠終止?

 

從上面的死循環輸出可以觀察到,

交換僅僅發生在 第一個數字第二個數字 之間。

如果我們嘗試繼續對 第二個數字 第三個數字 進行交換。

然后遞歸重復上面這個過程,

1 2 3   //初始值,第一個周期開始

2 1 3   //前兩個數字做交換
2 3 1   //后兩個數字做交換

...  遞歸  ...

3 2 1   //前兩個數字做交換
3 1 2   //后兩個數字做交換

...  遞歸  ...

1 3 2   //前兩個數字做交換
1 2 3   //后兩個數字做交換,進入下一個周期
...  遞歸  ...

... 無限循環 ...

你會發現所有的排列就可以被打印出來了,但是個死循環的打印。

 

如何終止?

只需要找到一個周期的開始特征即可,比如在上面的打印中

初始值是 1 2 3

而所有全排列情況打印完后,下個周期的開始也是 1 2 3

那么可以來個判斷a b c 變量是否同時和一開始一樣,如果是就退出函數返回。

 

解決兩個問題后,修改一下output函數:

void output(int a,int b,int c){
   
   printf("%d%d%d\n",a,b,c); //打印初始值

   swap(&a,&b);    //交換前兩個數字
   printf("%d%d%d\n",a,b,c);

   swap(&b,&c);    //交換后兩個數字

   //一個周期結束的情況發生在這個位置
   if(a==1 && b==2)return;

   output(a,b,c);   //遞歸

}

 

然后在main函數里面 執行 output(1,2,3) 輸出結果如下:

當然為了好看,你可以把交換的順序改下,先交換后面兩個數字,再交換首尾兩個數字

 

現在,3的全排列打印出來了,現在我們嘗試寫個代碼可以打印任意一個數的全排列

首先,假如打印4的全排列,如果繼續使用上面的代碼思路,那么將需要修改:

output(1,2,3)   =>  output(1,2,3,4);

printf("%d%d%d\n")  =>  printf("%d%d%d%d\n");  

 

很多地方需要像上面一樣添加一個變量,這樣的話打印100的全排列,豈不是要寫100個變量 或者 參數??

這樣很顯然不科學,因此我們換一個問題:輸入一個數字,然后打印這個數字的全排列。也就是:

上面原版是:輸入三個數字,打印這三個數字全排列,output(1,2,3);

而現在改成:輸入一個數字,打印這個數字的全排列,output(3),需要輸出結果與上面相同。

 

現在參數變成了一個,要通過這個參數能將1 2 3 成員表示出來

要能交換這三個數字的其中兩個,且不能使用變量多個變量,如何實現呢?

首先我們需要把 1 2 3 這三個數字通過 3 這個數字得到,並將其保存到數組里,很簡單:

int i,arr[1024];
for(i=1;i<=3;i++)
   arr[i] = i;   //為了方便操作我們從1開始存儲

 

然后遍歷數組,對這個數組遍歷輸出一次,然后交換一次

int i,j;
for(i=1;i<3;i++){
 
   //輸出當前的數字組合
   for(j=1;j<=3;j++)
      printf("%d",arr[j]);
   printf("\n");

   //交換數組兩個數字
   swap(&arr[i],&arr[i+1]);

}

 

我們嘗試執行 output(3)  ,結果輸出如下

 

我們稍微做下修改,

1、當 i 遍歷到數組的最后一個元素時,與第一個元素交換

2、將 3 統一用參數n來表示

於是output函數就可以寫成

void output(int n){
  int i,j,arr[1024];
  for(i=1;i<=n;i++)    //保存n的全排列成員
     arr[i] = i;

  for(i=1;i<=n;i++){   //遍歷數組

//輸出當前數組組合
for(j=1;j<=n;j++) printf("%d",arr[j]); printf("\n");

//交換數組的成員位置 if(i==n) swap(&arr[i],&ar
r[1]); //當遍歷到最后一個時,與第一個成員做交換
else swap(
&arr[i],&arr[i+1]); //否則與下一個元素做交換 } }

 

那么假如n=3,將數組遍歷一次的過程如下:

1 2 3  //初始狀態 , i=1
2 1 3  //交換,i=2
2 3 1  //交換,i=3
1 3 2  //交換,i=4,退出循環

 

很顯然遍歷一次是不足以將所有的排列情況輸出出來。

為了使得數組能夠繼續從下標1為開始遍歷,思路就是使用遞歸

也就是第一次遍歷完后,將遍歷完后的數組 遞歸傳給函數本身繼續遍歷

而要保持數組的狀態,意味着數組要以參數的形式傳遞,其次數組的初始化不能再遞歸里初始。

所以初始化可以放在main函數里,當然既然用到了遞歸,就需要防止無休止的遞歸,也就是要找到退出狀態

不妨我們再寫代碼之前分析一下:

傳入數組
arr = [0, 1, 2, 3]  //0下標不使用,下面忽略

//開始遍歷
[ 1, 2, 3]    // i=1,輸出然后交換
[ 2, 1, 3]    // i=2,輸出然后交換
[ 2, 3, 1]    // i=3,輸出然后與第一個元素交換
[ 1, 3, 2]    // i=4,  退出循環

-----遞歸-----

傳入數組
arr = [0, 1, 3, 2]  //保存上次的狀態繼續任務,0繼續忽略

//開始遍歷
[ 1, 3, 2]    // i=1 輸出然后交換
[ 3, 1, 2]    // i=2 輸出然后交換
[ 3, 2, 1]    // i=3 輸出然后與第一個元素交換
[ 1, 2, 3]    // i=4 退出循環,至此我們不需要繼續執行了

---因此需要在此寫個遞歸結束判斷條件----

 

遞歸的結束條件與上面思路一樣,如果數組與最最開始的狀態是一樣的

那么表示一個周期已經完成:

int tag = 1;          //標志位 for(i=1;i<=n;i++){
   if(arr[i]!=i){     //如果存在不同繼續遞歸
     tag = 0;
     break;
  }
}
if(tag)return //如果全相同退出遞歸

 

那么總的代碼如下:

#include <stdio.h>
#include <stdlib.h>
#define MAX 1024

void swap(int *a,int *b){
  int tmp = *a;
  *a = *b;
  *b = tmp;
  return;
}
void output(int n,int arr[]){
  int i,j;
  for(i=1;i<=n;i++){

    //print
    for(j=1;j<=n;j++){
      printf("%d ",arr[j]);
    }
    printf("\n");

    //swap
    if(i==n)swap(&arr[i],&arr[1]);
    else swap(&arr[i],&arr[i+1]);

  }

  int tag = 1;
  for(i=1;i<=n;i++)
    if(arr[i]!=i){tag=0;break;}

  if(tag)return;

  output(n,arr);
}

int main(){
  int i;
  int n,arr[MAX];
  printf("enter a num:");
  scanf("%d",&n);
  for(i=1;i<=n;i++)
    arr[i]=i;

  output(n,arr);
  return 0;
}

執行結果

 

對於高位數的全排列,上面的思路不適合,其實打印全排列主要需要考慮的是如何安排數字位置之間

的交換,使得每種情況都可以遍歷到,想要避免非重復遍歷是不可能的,除非有一個很復雜的函數計算

公式,否則單純的位置交換一定會有不斷的重復排列某一種狀態,但只要保證打印出來的時候,每個打印

結果不想同即可,如下一段代碼,打印7的全排列, 7*6*5*4*3*2*1 = 5040種情況,由於本人能力有限

設計不出來一個可以打印全排列的完美情況,所以下面參考網上文章的代碼:

#include <stdio.h>
#include <stdlib.h>
int n=0;

void swap(int *a, int *b){
     int m;
     m=*a;
     *a=*b;
     *b=m;
}
void perm(int list[], int k, int m){
     int i;
     if(k==m){
       for(i=0;i<=m;i++)
         printf("%d ",list[i]);
         printf("\n");
         n++;
     } else {
       for(i=k;i<=m;i++){
         swap(&list[k],&list[i]);
         perm(list, k+1, m);
         swap(&list[k], &list[i]);
       }
     }
}       
int main(void){
    int list[]={1,2,3,4,5,6,7};
    perm(list,0,6);
    printf("total:%d\n",n);return 0;
}

 

代碼運行時流程如下:

1.只有k=6時才能夠打印
2.遞歸到最深處時,k=6
3.利用循環使得k及其身后的數字兩兩相鄰交換
4.彈出條件是k后面的數字都完成了相鄰交換,也恢復了相對初狀態
5.每次彈出后馬上要做的就是恢復狀態
6.不斷回溯k重復上面的步驟


一開始 遞歸k直至進入6  打印初狀態 1,2,3,4,5,6,7

回溯 k 到5 ,利用循環讓 i=6 交換5,6 遞歸到底打印出來,彈出再把5,6換回來 回到初狀態    打印:1,2,3,4,5,7,6    換回:1,2,3,4,5,6,7

回溯 k 到4 ,利用循環讓 i=5 交換4,5 遞歸到底打印出來                              打印:1,2,3,4,6,5,7

彈出 k 到5 ,利用循環讓 i=6 交換5,6 遞歸到底打印出來,彈出再把 5,6 換回來             打印:1,2,3,4,6,7,5    換回:1,2,3,4,6,5,7

彈出 k 為4 ,換回4和5,繼續循環                                                 換回:1,2,3,4,5,6,7

k=4 循環使得 i=6 交換4,6值,遞歸到底打印出來,                                     打印:1,2,3,4,7,6,5    

彈出 k 到5 ,利用循環讓 i=6 交換5,6 遞歸到底打印出來,彈出再把5,6換回來                打印:1,2,3,4,7,5,6    換回:1,2,3,4,7,6,5

彈出 k 到4 ,換回4,6,循環結束,完成任務繼續回溯                                 換回:1,2,3,4,5,6,7

以此類推。。。

 

下面是部分全排列截圖

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM