作者:bakari
時間:2012.8.2-23:48
轉載請注明出處:http://www.cnblogs.com/bakari/archive/2012/08/02/2620826.html 謝謝!
全排列在近幾年各大網絡公司的筆試中出現的比較頻繁
首先來看看題目是如何要求的(百度迅雷校招筆試題)。
用C++寫一個函數, 如 Foo(const char *str), 打印出 str 的全排列,
如 abc 的全排列: abc, acb, bca, dac, cab, cba
一、 遞歸版本
1、算法簡述
簡單地說:就是第一個數分別以后面的數進行交換
E.g:E = (a , b , c),則 prem(E)= a.perm(b,c)+ b.perm(a,c)+ c.perm(a,b)
然后a.perm(b,c)= ab.perm(c)+ ac.perm(b)= abc + acb.依次遞歸進行
好了,知道算法之后就不難編出一份好的代碼了。
2、代碼參考
1 Foo(const char *str) 2 { 3 Perm( str , 0 , strlen( str ) – 1 ); 4 }
1 //需要三個參數,k表示當前的數,m表示數的個數 2 Perm( char *pszStr , int k , int m ) 3 { 4 if (k == m) 5 { 6 static int s_i = 1; 7 cout<<” 第 ”<<s_i ++<<” 個排列 ”<<pszStr<<endl; 8 } 9 else 10 { 11 for (int i = k; i <= m; i++) //第i個數分別與它后面的數字交換就能得到新的排列 12 { 13 Swap(pszStr + k, pszStr + i); 14 Perm(pszStr, k + 1, m); 15 Swap(pszStr + k, pszStr + i); 16 } 17 } 18 }
3、見圖知曉
不過這樣存在一點小小的缺陷:兩個相同的數也進行了交換,見下圖:
明顯,這絕對不符合要求:
4、代碼改進
去掉重復符號的全排列:在交換之前可以先判斷兩個符號是否相同,不相同才交換,這個時候需要一個判斷符號是否相同的函數。
bool IsSwap(char *pszStr, int nBegin, int nEnd) { for (int i = nBegin; i < nEnd; i++) if (pszStr[i] == pszStr[nEnd]) return false; return true; }
所以,改進的代碼如下:
1 Perm(char *pszStr, int k, int m) 2 { 3 if (k == m) 4 { 5 Static int s_i = 1; 6 cout<<” 第 ”<<s_i ++<<” 個排列 ”<<pszStr<<endl; 7 } 8 else 9 { 10 for (int i = k; i <= m; i++) //第i個數分別與它后面的數字交換就能得到新的排列 11 { 12 if (IsSwap(pszStr, k, i)) //添加的判斷語句,判斷是否相等 13 { 14 Swap(pszStr + k, pszStr + i); 15 Perm(pszStr, k + 1, m); 16 Swap(pszStr + k, pszStr + i); 17 } 18 } 19 } 20 }
OK,見圖知情況
二、 非遞歸版本
1、算法簡述
要考慮全排列的非遞歸實現,先來考慮如何計算字符串的下一個排列。如"1234"的下一個排列就是"1243"。只要對字符串反復求出下一個排列,全排列的也就迎刃而解了。
如何計算字符串的下一個排列了?來考慮"926520"這個字符串,我們從后向前找第一雙相鄰的遞增數字,"20"、"52"都是非遞增的,"26 "即滿足要求,稱前一個數字2為替換數,替換數的下標稱為替換點,再從后面找一個比替換數大的最小數(這個數必然存在),0、2都不行,5可以,將5和2交換得到"956220",然后再將替換點后的字符串"6220"顛倒即得到"950226"。
如果達到這個數的最大,比如1234-à4321,這個時候就結束整個循環。
如果輸入是一個非最小數,如1324,則將它轉換為最小數,如1234,再進行排序。排序算法用快排,可以自己寫一個,如果快排不會的話,就先看會再來接着看,或者自己想一個靠譜的算法,也可以直接用VC庫中的qsort(s , n , sizeof(s[0]) , cmp);各參數是什么意思就自己在下面多花點時間吧。
OK,下面看代碼分析
2、代碼分析
1 Prem( char *s ) //全排列函數 2 { 3 char *pEnd = s + strlen(s) - 1; 4 char *p = pEnd; //p代表替換點 5 //q代表替換點的下一個數 ,pMax 代表替換點后比替換點大的最小數 6 char *q = new char,*pMax = new char; //注意初始化!!! 7 while (p != s) //p == s 就結束循環 8 { 9 q = p; 10 p--; 11 if (*p < *q) 12 { 13 pMax = FindMaxForOne(p,pEnd); //找與替換點交換的點 14 Swap(p,pMax); //交換 15 Reverse(q,pEnd); //將替換點后所有數進行反轉 16 Print(s); //輸出 17 p = pEnd; //將替換點置最后一個點,開始下一輪循環 18 } 19 if (s == p) break; //結束條件 20 } 21 }
上面涉及到幾個函數
說一下找與替換數交換的數的函數
1 char* FindMaxForOne(char *p,char *q) 2 { 3 char *p1 = p; 4 char *p2 = q; 5 while (*p2 <= *p1) p2--; 6 return p2; 7 }
!!!這里要說明:從后往前找的第一個比替換數大的數一定就是要找的最小數,Why,這個慢慢品味,我做的時候也遇到一定的困難,自己去做,不經歷就不會輕易銘記。
其他函數簡直就是小case了。祝君成功!
3、見圖知曉
三、非遞歸還有一種方法
描述:和上一種不同的是:這種算法比較笨,但很好理解,不用按照上一種那么嚴格從小到大進行排列輸出。
首先先將最后一個數從右往左依次交換輸出,然后判斷個數是否為基數,交換離該數最遠端的兩個數,再把第一個數從左往右交換輸出,交換遠端的兩個數,如此進行循環就能排列完全部的數。這說得可能比較抽象,看一個例子:
E.g: 1 2 3 4
第一次:(從右往左):1 2 4 3 --- 1 2 4 3 --- 1 4 2 3 --- 4 1 2 3 把最后一個數依次往前移
交換:2 和 3 ---> 4 1 3 2
第二次:(從左往右):4 1 3 2 --- 1 4 3 2 --- 1 3 4 2 --- 1 3 2 4 把第一個數依次往后移
交換:1 和 3 ----> 3 1 2 4 重復第一次,知道把所有數輸出為止
看代碼:
1 /************************************************************************ 2 * Author: bakari 3 * Date: 2011.5.7 4 /************************************************************************/ 5 int n; 6 void swap(int *a,int *b); //交換函數 7 void print(int a[]); //打印交換后的每一組數 8 int jfc(); //求階乘函數 9 int jmp(int n); //跳轉函數 10 void sort(int a[]); //全排列函數 11 12 int main(){ 13 while(cin>>n) 14 { 15 while(n<=0) 16 { 17 cout<<"輸入有誤!請重新輸入: "; 18 cin>>n; 19 } 20 int *a=new int[n]; 21 for(int i=0;i<n;i++) 22 a[i]=i+1; 23 sort(a); 24 delete []a; 25 } 26 system("pause"); 27 return 0; 28 } 29 30 void swap(int *a,int *b) 31 { 32 int t=*a; 33 *a=*b; 34 *b=t; 35 } 36 void print(int a[]) 37 { 38 for(int i=0;i<n;i++) 39 cout<<a[i]<<' '; 40 cout<<endl; 41 42 } 43 int jfc() 44 { 45 int s=1; 46 for(int i=1;i<=n;i++) 47 s*=i; 48 return s; 49 } 50 int jmp(int n) 51 { 52 if(n>jfc()) 53 return 0; 54 } 55 void sort(int a[]) 56 { 57 int m=1,count=0; //m統計全排列的個數,count統計行數 58 int *p1,*p2; 59 for(p1=a+n-1,p2=a+n-2;p1>=a+1,p2>=a;p1--,p2--) 60 { 61 print(a); 62 swap(p1,p2); 63 m++; 64 } 65 count++; 66 while(m<=jfc()){ 67 if(count%2) 68 { print(a); 69 swap(&a[n-1],&a[n-2]); 70 m++; 71 if(!jmp(m)) 72 break; 73 for(p1=a,p2=a+1;p1<=a+n-2,p2<=a+n-1;p1++,p2++) 74 { 75 print(a); 76 swap(p1,p2); 77 m++; 78 } 79 count++; 80 } 81 else 82 { 83 print(a); 84 swap(&a[0],&a[1]); 85 m++; 86 if(!jmp(m)) 87 break; 88 for(p1=a+n-1,p2=a+n-2;p1>=a+1,p2>=a;p1--,p2--) 89 { 90 print(a); 91 swap(p1,p2); 92 m++; 93 } 94 count++; 95 } 96 97 } 98 cout<<"共有"<<m-1<<"種排列"<<endl; 99 }
關鍵是掌握上面兩種!
四、 總結
至此我們已經運用了遞歸與非遞歸的方法解決了全排列問題,總結一下就是:
1.全排列就是從第一個數字起每個數分別與它后面的數字交換。
2.去重的全排列就是從第一個數字起每個數分別與它后面非重復出現的數字交換。
3.全排列的非遞歸就是由后向前找替換數和替換點,然后由后向前找第一個比替換數大的數與替換數交換,最后顛倒替換點后的所有數據。
-----寫於2012/8/2--23:48
希望大家多多燒香!
開了個公眾號「aCloudDeveloper」,專注技術干貨分享,期待與你相遇。