全排列在筆試面試中很熱門,因為它難度適中,既可以考察遞歸實現,又能進一步考察非遞歸的實現,便於區分出考生的水平。所以在百度和迅雷的校園招聘中都會考到。。
首先來看看題目是如何要求的(百度迅雷校招筆試題)。
題目:用C++寫一個函數, 如 Foo(const char *str), 打印出 str 的全排列, 如 abc 的全排列: abc, acb, bca, dac, cab, cba。。
一、全排列的遞歸實現
為方便起見,用123來示例下。123的全排列有123、132、213、231、312、321這六種。首先考慮213和321這二個數是如何得出的。顯然這二個都是123中的1與后面兩數交換得到的。然后可以將123的第二個數和每三個數交換得到132。同理可以根據213和321來得231和312。因此可以知道——全排列就是從第一個數字起每個數分別與它后面的數字交換。找到這個規律后,遞歸的代碼就很容易寫出來了:
#include<iostream> using namespace std; void Swap(char *a,char *b) { char tmp=*a; *a=*b; *b=tmp; } void AllRange(char *pszStr,int k,int m) { if(k==m) { static int s_i=1; printf("第%d個排列是%s\n",s_i++,pszStr); } else { for(int i=k;i<=m;i++) { Swap(pszStr+i,pszStr+k); AllRange(pszStr,k+1,m); Swap(pszStr+i,pszStr+k); } } } void Foo(char *pszStr) { AllRange(pszStr,0,strlen(pszStr)-1); } int main() { printf("全排列的遞歸實現:\n"); char szTextStr[] = "123"; printf("%s的全排列如下:\n", szTextStr); Foo(szTextStr); system("pause"); return 0; }
運行結果如下圖所示:
注意這樣的方法沒有考慮到重復數字,如122將會輸出:
這種輸出絕對不符合要求,因此現在要想辦法來去掉重復的數列。
二、去掉重復數字的全排列遞歸實現
由於全排列就是從第一個數字起每個數分別與它后面的數字交換。我們先嘗試加個這樣的判斷——如果一個數與后面的數字相同那么這二個數就不交換了。如122,第一個數與后面交換得212、221。然后122中第二數就不用與第三個數交換了,但對212,它第二個數與第三個數是不相同的,交換之后得到221。與由122中第一個數與第三個數交換所得的221重復了。所以這個方法不行。
換種思維,對122,第一個數1與第二個數2交換得到212,然后考慮第一個數1與第三個數2交換,此時由於第三個數等於第二個數,所以第一個數不再與第三個數交換。再考慮212,它的第二個數與第三個數交換可以得到解決221。此時全排列生成完畢。
這樣我們也得到了在全排列中去掉重復的規則——去重的全排列就是從第一個數字起每個數分別與它后面非重復出現的數字交換。用編程的話描述就是第i個數與第j個數交換時,要求[i,j)中沒有與第j個數相等的數。下面給出完整代碼:
#include<iostream> using namespace std; void Swap(char *a,char *b) { char tmp=*a; *a=*b; *b=tmp; } //在pszStr數組中,[nBegin,nEnd)中是否有數字與下標為nEnd的數字相等 bool IsSwap(char *pszStr, int nBegin, int nEnd) { for (int i = nBegin; i < nEnd; i++) if (pszStr[i] == pszStr[nEnd]) return false; return true; } //k表示當前選取到第幾個數,m表示共有多少數. void AllRange(char *pszStr,int k,int m) { if(k==m) { static int s_i=1; printf("第%d個排列是%s\n",s_i++,pszStr); } else { for(int i=k;i<=m;i++) { if (IsSwap(pszStr,k,i)) { Swap(pszStr+i,pszStr+k); AllRange(pszStr,k+1,m); Swap(pszStr+i,pszStr+k); } } } } void Foo(char *pszStr) { AllRange(pszStr,0,strlen(pszStr)-1); } int main() { printf("全排列的遞歸實現:\n"); char szTextStr[] = "122"; printf("%s的全排列如下:\n", szTextStr); Foo(szTextStr); system("pause"); return 0; }
OK,到現在我們已經能熟練寫出遞歸的方法了,並且考慮了字符串中的重復數據可能引發的重復數列問題。那么如何使用非遞歸的方法來得到全排列了?
三、全排列的費遞歸實現
要考慮全排列的非遞歸實現,先來考慮如何計算字符串的下一個排列。如"1234"的下一個排列就是"1243"。只要對字符串反復求出下一個排列,全排列的也就迎刃而解了。
如何計算字符串的下一個排列了?來考慮"926520"這個字符串,我們從后向前找第一雙相鄰的遞增數字,"20"、"52"都是非遞增的,"26 "即滿足要求,稱前一個數字2為替換數,替換數的下標稱為替換點,再從后面找一個比替換數大的最小數(這個數必然存在),0、2都不行,5可以,將5和2交換得到"956220",然后再將替換點后的字符串"6220"顛倒即得到"950226"。
對於像"4321"這種已經是最“大”的排列,采用STL中的處理方法,將字符串整個顛倒得到最“小”的排列"1234"並返回false。
這樣,只要一個循環再加上計算字符串下一個排列的函數就可以輕松的實現非遞歸的全排列算法。按上面思路並參考STL中的實現源碼,不難寫成一份質量較高的代碼。值得注意的是在循環前要對字符串排序下,可以自己寫快速排序的代碼(請參閱《白話經典算法之六 快速排序 快速搞定》),也可以直接使用VC庫中的快速排序函數(請參閱《使用VC庫函數中的快速排序函數》)。下面列出完整代碼:
//全排列的非遞歸實現 #include <stdio.h> #include <stdlib.h> #include <string.h> void Swap(char *a, char *b) { char t = *a; *a = *b; *b = t; } //反轉區間 void Reverse(char *a, char *b) { while (a < b) Swap(a++, b--); } //下一個排列 bool Next_permutation(char a[]) { char *pEnd = a + strlen(a); if (a == pEnd) return false; char *p, *q, *pFind; pEnd--; p = pEnd; while (p != a) { q = p; --p; if (*p < *q) //找降序的相鄰2數,前一個數即替換數 { //從后向前找比替換點大的第一個數 pFind = pEnd; while (*pFind <= *p) --pFind; //替換 Swap(pFind, p); //替換點后的數全部反轉 Reverse(q, pEnd); return true; } } Reverse(p, pEnd);//如果沒有下一個排列,全部反轉后返回true return false; } int QsortCmp(const void *pa, const void *pb) { return *(char*)pa - *(char*)pb; } int main() { printf("全排列的非遞歸實現\n"); char szTextStr[] = "abc"; printf("%s的全排列如下:\n", szTextStr); //加上排序 qsort(szTextStr, strlen(szTextStr), sizeof(szTextStr[0]), QsortCmp); int i = 1; do{ printf("第%3d個排列\t%s\n", i++, szTextStr); }while (Next_permutation(szTextStr)); system("pause"); return 0; }
至此我們已經運用了遞歸與非遞歸的方法解決了全排列問題,總結一下就是:
1.全排列就是從第一個數字起每個數分別與它后面的數字交換。
2.去重的全排列就是從第一個數字起每個數分別與它后面非重復出現的數字交換。
3.全排列的非遞歸就是由后向前找替換數和替換點,然后由后向前找第一個比替換數大的數與替換數交換,最后顛倒替換點后的所有數據。