字符串的全排列和組合算法

 

字符串的全排列和組合算法

分類: 面試珠璣 24316人閱讀 評論(26) 收藏 舉報
全排列在筆試面試中很熱門,因爲它難度適中,既可以考察遞歸實現,又能進一步考察非遞歸的實現,便於區分出考生的水平。所以在百度和迅雷的校園招聘以及程序員和軟件設計師的考試中都考到了,因此本文對全排列作下總結幫助大家更好的學習和理解。對本文有任何補充之處,歡迎大家指出。
首先來看看題目是如何要求的(百度迅雷校招筆試題)。
一、字符串的排列
用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。因此可以知道——全排列就是從第一個數字起每個數分別與它後面的數字交換。找到這個規律後,遞歸的代碼就很容易寫出來了:

  1. #include<iostream>  
  2. using namespace std;  
  3. #include<assert.h>  
  4.   
  5. void Permutation(char* pStr, char* pBegin)  
  6. {  
  7.     assert(pStr && pBegin);  
  8.   
  9.     if(*pBegin == '\0')  
  10.         printf("%s\n",pStr);  
  11.     else  
  12.     {  
  13.         for(char* pCh = pBegin; *pCh != '\0'; pCh++)  
  14.         {  
  15.             swap(*pBegin,*pCh);  
  16.             Permutation(pStr, pBegin+1);  
  17.             swap(*pBegin,*pCh);  
  18.         }  
  19.     }  
  20. }  
  21.   
  22. int main(void)  
  23. {  
  24.     char str[] = "abc";  
  25.     Permutation(str,str);  
  26.     return 0;  
  27. }  

另外一種寫法:

  1. //k表示當前選取到第幾個數,m表示共有多少個數  
  2. void Permutation(char* pStr,int k,int m)  
  3. {  
  4.     assert(pStr);  
  5.   
  6.     if(k == m)  
  7.     {  
  8.         static int num = 1;  //局部靜態變量,用來統計全排列的個數  
  9.         printf("第%d個排列\t%s\n",num++,pStr);  
  10.     }  
  11.     else  
  12.     {  
  13.         for(int i = k; i <= m; i++)  
  14.         {  
  15.             swap(*(pStr+k),*(pStr+i));  
  16.             Permutation(pStr, k + 1 , m);  
  17.             swap(*(pStr+k),*(pStr+i));  
  18.         }  
  19.     }  
  20. }  
  21.   
  22. int main(void)  
  23. {  
  24.     char str[] = "abc";  
  25.     Permutation(str , 0 , strlen(str)-1);  
  26.     return 0;  
  27. }  
如果字符串中有重複字符的話,上面的那個方法肯定不會符合要求的,因此現在要想辦法來去掉重複的數列。
二、去掉重複的全排列的遞歸實現
由於全排列就是從第一個數字起每個數分別與它後面的數字交換。我們先嚐試加個這樣的判斷——如果一個數與後面的數字相同那麼這二個數就不交換了。如122,第一個數與後面交換得212、221。然後122中第二數就不用與第三個數交換了,但對212,它第二個數與第三個數是不相同的,交換之後得到221。與由122中第一個數與第三個數交換所得的221重複了。所以這個方法不行。

換種思維,對122,第一個數1與第二個數2交換得到212,然後考慮第一個數1與第三個數2交換,此時由於第三個數等於第二個數,所以第一個數不再與第三個數交換。再考慮212,它的第二個數與第三個數交換可以得到解決221。此時全排列生成完畢。
這樣我們也得到了在全排列中去掉重複的規則——去重的全排列就是從第一個數字起每個數分別與它後面非重複出現的數字交換。下面給出完整代碼:

  1. #include<iostream>  
  2. using namespace std;  
  3. #include<assert.h>  
  4.   
  5. //在[nBegin,nEnd)區間中是否有字符與下標爲pEnd的字符相等  
  6. bool IsSwap(char* pBegin , char* pEnd)  
  7. {  
  8.     char *p;  
  9.     for(p = pBegin ; p < pEnd ; p++)  
  10.     {  
  11.         if(*p == *pEnd)  
  12.             return false;  
  13.     }  
  14.     return true;  
  15. }  
  16. void Permutation(char* pStr , char *pBegin)  
  17. {  
  18.     assert(pStr);  
  19.   
  20.     if(*pBegin == '\0')  
  21.     {  
  22.         static int num = 1;  //局部靜態變量,用來統計全排列的個數  
  23.         printf("第%d個排列\t%s\n",num++,pStr);  
  24.     }  
  25.     else  
  26.     {  
  27.         for(char *pCh = pBegin; *pCh != '\0'; pCh++)   //第pBegin個數分別與它後面的數字交換就能得到新的排列     
  28.         {  
  29.             if(IsSwap(pBegin , pCh))  
  30.             {  
  31.                 swap(*pBegin , *pCh);  
  32.                 Permutation(pStr , pBegin + 1);  
  33.                 swap(*pBegin , *pCh);  
  34.             }  
  35.         }  
  36.     }  
  37. }  
  38.   
  39. int main(void)  
  40. {  
  41.     char str[] = "baa";  
  42.     Permutation(str , str);  
  43.     return 0;  
  44. }  
OK,到現在我們已經能熟練寫出遞歸的方法了,並且考慮了字符串中的重複數據可能引發的重複數列問題。那麼如何使用非遞歸的方法來得到全排列了?

三、全排列的非遞歸實現
要考慮全排列的非遞歸實現,先來考慮如何計算字符串的下一個排列。如"1234"的下一個排列就是"1243"。只要對字符串反覆求出下一個排列,全排列的也就迎刃而解了。
如何計算字符串的下一個排列了?來考慮"926520"這個字符串,我們從後向前找第一雙相鄰的遞增數字,"20"、"52"都是非遞增的,"26 "即滿足要求,稱前一個數字2爲替換數,替換數的下標稱爲替換點,再從後面找一個比替換數大的最小數(這個數必然存在),0、2都不行,5可以,將5和2交換得到"956220",然後再將替換點後的字符串"6220"顛倒即得到"950226"。
對於像“4321”這種已經是最“大”的排列,採用STL中的處理方法,將字符串整個顛倒得到最“小”的排列"1234"並返回false。

這樣,只要一個循環再加上計算字符串下一個排列的函數就可以輕鬆的實現非遞歸的全排列算法。按上面思路並參考STL中的實現源碼,不難寫成一份質量較高的代碼。值得注意的是在循環前要對字符串排序下,可以自己寫快速排序的代碼(請參閱《白話經典算法之六 快速排序 快速搞定》),也可以直接使用VC庫中的快速排序函數(請參閱《使用VC庫函數中的快速排序函數》)。下面列出完整代碼:

  1. #include<iostream>  
  2. #include<algorithm>  
  3. #include<cstring>  
  4. using namespace std;  
  5. #include<assert.h>  
  6.   
  7. //反轉區間  
  8. void Reverse(char* pBegin , char* pEnd)  
  9. {  
  10.     while(pBegin < pEnd)  
  11.         swap(*pBegin++ , *pEnd--);  
  12. }  
  13. //下一個排列  
  14. bool Next_permutation(char a[])  
  15. {  
  16.     assert(a);  
  17.     char *p , *q , *pFind;  
  18.     char *pEnd = a + strlen(a) - 1;  
  19.     if(a == pEnd)  
  20.         return false;  
  21.     p = pEnd;  
  22.     while(p != a)  
  23.     {  
  24.         q = p;  
  25.         p--;  
  26.         if(*p < *q)  //找降序的相鄰2數,前一個數即替換數    
  27.         {  
  28.              //從後向前找比替換點大的第一個數  
  29.             pFind = pEnd;  
  30.             while(*pFind < *p)  
  31.                 --pFind;  
  32.             swap(*p , *pFind);  
  33.             //替換點後的數全部反轉  
  34.             Reverse(q , pEnd);  
  35.             return true;  
  36.         }  
  37.     }  
  38.     Reverse(a , pEnd);   //如果沒有下一個排列,全部反轉後返回false     
  39.     return false;  
  40. }  
  41.   
  42. int cmp(const void *a,const void *b)  
  43. {  
  44.     return int(*(char *)a - *(char *)b);  
  45. }  
  46. int main(void)  
  47. {  
  48.     char str[] = "bac";  
  49.     int num = 1;  
  50.     qsort(str , strlen(str),sizeof(char),cmp);  
  51.     do  
  52.     {  
  53.         printf("第%d個排列\t%s\n",num++,str);   
  54.     }while(Next_permutation(str));  
  55.     return 0;  
  56. }  
至此我們已經運用了遞歸與非遞歸的方法解決了全排列問題,總結一下就是:
1、全排列就是從第一個數字起每個數分別與它後面的數字交換。
2、去重的全排列就是從第一個數字起每個數分別與它後面非重複出現的數字交換。
3、全排列的非遞歸就是由後向前找替換數和替換點,然後由後向前找第一個比替換數大的數與替換數交換,最後顛倒替換點後的所有數據。

二、字符串的組合

題目:輸入一個字符串,輸出該字符串中字符的所有組合。舉個例子,如果輸入abc,它的組合有a、b、c、ab、ac、bc、abc。

上面我們詳細討論瞭如何用遞歸的思路求字符串的排列。同樣,本題也可以用遞歸的思路來求字符串的組合。

假設我們想在長度爲n的字符串中求m個字符的組合。我們先從頭掃描字符串的第一個字符。針對第一個字符,我們有兩種選擇:第一是把這個字符放到組合中去,接下來我們需要在剩下的n-1個字符中選取m-1個字符;第二是不把這個字符放到組合中去,接下來我們需要在剩下的n-1個字符中選擇m個字符。這兩種選擇都很容易用遞歸實現。下面是這種思路的參考代碼:
  1. #include<iostream>  
  2. #include<vector>  
  3. #include<cstring>  
  4. using namespace std;  
  5. #include<assert.h>  
  6.   
  7. void Combination(char *string ,int number,vector<char> &result);  
  8.   
  9. void Combination(char *string)  
  10. {  
  11.     assert(string != NULL);  
  12.     vector<char> result;  
  13.     int i , length = strlen(string);  
  14.     for(i = 1 ; i <= length ; ++i)  
  15.         Combination(string , i ,result);  
  16. }  
  17.   
  18. void Combination(char *string ,int number , vector<char> &result)  
  19. {  
  20.     assert(string != NULL);  
  21.     if(number == 0)  
  22.     {  
  23.         static int num = 1;  
  24.         printf("第%d個組合\t",num++);  
  25.   
  26.         vector<char>::iterator iter = result.begin();  
  27.         for( ; iter != result.end() ; ++iter)  
  28.             printf("%c",*iter);  
  29.         printf("\n");  
  30.         return ;  
  31.     }  
  32.     if(*string == '\0')  
  33.         return ;  
  34.     result.push_back(*string);  
  35.     Combination(string + 1 , number - 1 , result);  
  36.     result.pop_back();  
  37.     Combination(string + 1 , number , result);  
  38. }  
  39.   
  40. int main(void)  
  41. {  
  42.     char str[] = "abc";  
  43.     Combination(str);  
  44.     return 0;  
  45. }  

由於組合可以是1個字符的組合,2個字符的字符……一直到n個字符的組合,因此在函數void Combination(char* string),我們需要一個for循環。另外,我們用一個vector來存放選擇放進組合裏的字符。
方法二:用位運算來實現求組合

  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. int a[] = {1,3,5,4,6};  
  5. char str[] = "abcde";  
  6.   
  7. void print_subset(int n , int s)  
  8. {  
  9.     printf("{");  
  10.     for(int i = 0 ; i < n ; ++i)  
  11.     {  
  12.         if( s&(1<<i) )         // 判斷s的二進制中哪些位爲1,即代表取某一位  
  13.             printf("%c ",str[i]);   //或者a[i]  
  14.     }  
  15.     printf("}\n");  
  16. }  
  17.   
  18. void subset(int n)  
  19. {  
  20.     for(int i= 0 ; i < (1<<n) ; ++i)  
  21.     {  
  22.         print_subset(n,i);  
  23.     }  
  24. }  
  25.   
  26.   
  27.   
  28. int main(void)  
  29. {  
  30.     subset(5);  
  31.     return 0;  
  32. }  

字符串全排列擴展----八皇后問題
    題目:在8×8的國際象棋上擺放八個皇后,使其不能相互攻擊,即任意兩個皇后不得處在同一行、同一列或者同一對角斜線上。下圖中的每個黑色格子表示一個皇后,這就是一種符合條件的擺放方法。請求出總共有多少種擺法。


    這就是有名的八皇后問題。解決這個問題通常需要用遞歸,而遞歸對編程能力的要求比較高。因此有不少面試官青睞這個題目,用來考察應聘者的分析複雜問題的能力以及編程的能力。

由於八個皇后的任意兩個不能處在同一行,那麼這肯定是每一個皇后佔據一行。於是我們可以定義一個數組ColumnIndex[8],數組中第i個數字表示位於第i行的皇后的列號。先把ColumnIndex的八個數字分別用0-7初始化,接下來我們要做的事情就是對數組ColumnIndex做全排列。由於我們是用不同的數字初始化數組中的數字,因此任意兩個皇后肯定不同列。我們只需要判斷得到的每一個排列對應的八個皇后是不是在同一對角斜線上,也就是數組的兩個下標i和j,是不是i-j==ColumnIndex[i]-Column[j]或者j-i==ColumnIndex[i]-ColumnIndex[j]。

關於排列的詳細討論,詳見上面的講解。
接下來就是寫代碼了。思路想清楚之後,編碼並不是很難的事情。下面是一段參考代碼:

  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. int g_number = 0;  
  5. void Permutation(int * , int  , int );  
  6. void Print(int * , int );  
  7.   
  8. void EightQueen( )  
  9. {  
  10.     const int queens = 8;  
  11.     int ColumnIndex[queens];  
  12.     for(int i = 0 ; i < queens ; ++i)  
  13.         ColumnIndex[i] = i;    //初始化  
  14.     Permutation(ColumnIndex , queens , 0);  
  15. }  
  16.   
  17. bool Check(int ColumnIndex[] , int length)  
  18. {  
  19.     int i,j;  
  20.     for(i = 0 ; i < length; ++i)  
  21.     {  
  22.         for(j = i + 1 ; j < length; ++j)  
  23.         {  
  24.             if( i - j == ColumnIndex[i] - ColumnIndex[j] || j - i == ColumnIndex[i] - ColumnIndex[j])   //在正、副對角線上  
  25.                 return false;  
  26.         }  
  27.     }  
  28.     return true;  
  29. }  
  30. void Permutation(int ColumnIndex[] , int length , int index)  
  31. {  
  32.     if(index == length)  
  33.     {  
  34.         if( Check(ColumnIndex , length) )   //檢測棋盤當前的狀態是否合法  
  35.         {  
  36.             ++g_number;  
  37.             Print(ColumnIndex , length);  
  38.         }  
  39.     }  
  40.     else  
  41.     {  
  42.         for(int i = index ; i < length; ++i)   //全排列  
  43.         {  
  44.             swap(ColumnIndex[index] , ColumnIndex[i]);  
  45.             Permutation(ColumnIndex , length , index + 1);  
  46.             swap(ColumnIndex[index] , ColumnIndex[i]);  
  47.         }  
  48.     }  
  49. }  
  50.   
  51. void Print(int ColumnIndex[] , int length)  
  52. {  
  53.     printf("%d\n",g_number);  
  54.     for(int i = 0 ; i < length; ++i)  
  55.         printf("%d ",ColumnIndex[i]);  
  56.     printf("\n");  
  57. }  
  58.   
  59. int main(void)  
  60. {  
  61.     EightQueen();  
  62.     return 0;  
  63. }  
轉載:http://zhedahht.blog.163.co

題目:輸入兩個整數n和m,從數列1,2,3...n中隨意取幾個數,使其和等於m,要求列出所有的組合。

  1. #include <iostream>  
  2. #include <list>  
  3. using namespace std;  
  4. list<int> list1;  
  5. void find_factor(int sum,int n)  
  6. {  
  7.     //遞歸出口  
  8.     if(n<=0||sum<=0)  
  9.         return;  
  10.     //輸出找到的數  
  11.     if(sum==n)  
  12.     {  
  13.         list1.reverse();  
  14.         for(list<int>::iterator iter=list1.begin();iter!=list1.end();iter++)  
  15.             cout<<*iter<<"+";  
  16.         cout<<n<<endl;  
  17.         list1.reverse();  
  18.     }  
  19.     list1.push_front(n);  
  20.     find_factor(sum-n,n-1);//n放在裏面  
  21.     list1.pop_front();  
  22.     find_factor(sum,n-1);//n不放在裏面  
  23. }  
  24.   
  25. int main(void)  
  26. {  
  27.     int sum,n;  
  28.     cin>>sum>>n;  
  29.     cout<<"所有可能的序列,如下:"<<endl;  
  30.     find_factor(sum,n);  
  31.     return 0;  
  32. }  


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章