從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],&arr[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
以此類推。。。
下面是部分全排列截圖
