1. 程式人生 > >STL系列之十 全排列(百度迅雷筆試題)

STL系列之十 全排列(百度迅雷筆試題)

全排列在筆試面試中很熱門,因為它難度適中,既可以考察遞迴實現,又能進一步考察非遞迴的實現,便於區分出考生的水平。所以在百度和迅雷的校園招聘以及程式設計師和軟體設計師的考試中都考到了,因此本文對全排列作下總結幫助大家更好的學習和理解。對本文有任何補充之處,歡迎大家指出。

首先來看看題目是如何要求的(百度迅雷校招筆試題)。

用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. //全排列的遞迴實現
  2. #include <stdio.h>
  3. #include <string.h>
  4. void Swap(char *a, char *b)
  5. {
  6. char t = *a;
  7. *a = *b;
  8. *b = t;
  9. }
  10. //k表示當前選取到第幾個數,m表示共有多少數.
  11. void AllRange
    (char *pszStr, int k, int m)
  12. {
  13. if (k == m)
  14. {
  15. static int s_i = 1;
  16. printf(" 第%3d個排列\t%s\n", s_i++, pszStr);
  17. }
  18. else
  19. {
  20. for (int i = k; i <= m; i++) //第i個數分別與它後面的數字交換就能得到新的排列
  21. {
  22. Swap(pszStr + k, pszStr + i);
  23. AllRange(pszStr, k + 1, m);
  24. Swap(pszStr + k, pszStr + i);
  25. }
  26. }
  27. }
  28. void Foo(char *pszStr)
  29. {
  30. AllRange(pszStr, 0
    , strlen(pszStr) - 1);
  31. }
  32. int main()
  33. {
  34. printf(" 全排列的遞迴實現\n");
  35. printf(" --by MoreWindows( http://blog.csdn.net/MoreWindows )--\n\n");
  36. char szTextStr[] = "123";
  37. printf("%s的全排列如下:\n", szTextStr);
  38. Foo(szTextStr);
  39. return 0;
  40. }

執行結果如下:

注意這樣的方法沒有考慮到重複數字,如122將會輸出:

這種輸出絕對不符合要求,因此現在要想辦法來去掉重複的數列。

二.去掉重複的全排列的遞迴實現

由於全排列就是從第一個數字起每個數分別與它後面的數字交換。我們先嚐試加個這樣的判斷——如果一個數與後面的數字相同那麼這二個數就不交換了。如122,第一個數與後面交換得212、221。然後122中第二數就不用與第三個數交換了,但對212,它第二個數與第三個數是不相同的,交換之後得到221。與由122中第一個數與第三個數交換所得的221重複了。所以這個方法不行。

換種思維,對122,第一個數1與第二個數2交換得到212,然後考慮第一個數1與第三個數2交換,此時由於第三個數等於第二個數,所以第一個數不再與第三個數交換。再考慮212,它的第二個數與第三個數交換可以得到解決221。此時全排列生成完畢。

這樣我們也得到了在全排列中去掉重複的規則——去重的全排列就是從第一個數字起每個數分別與它後面非重複出現的數字交換。用程式設計的話描述就是第i個數與第j個數交換時,要求[i,j)中沒有與第j個數相等的數。下面給出完整程式碼:

  1. //去重全排列的遞迴實現
  2. #include <stdio.h>
  3. #include <string.h>
  4. void Swap(char *a, char *b)
  5. {
  6. char t = *a;
  7. *a = *b;
  8. *b = t;
  9. }
  10. //在pszStr陣列中,[nBegin,nEnd)中是否有數字與下標為nEnd的數字相等
  11. bool IsSwap(char *pszStr, int nBegin, int nEnd)
  12. {
  13. for (int i = nBegin; i < nEnd; i++)
  14. if (pszStr[i] == pszStr[nEnd])
  15. return false;
  16. return true;
  17. }
  18. //k表示當前選取到第幾個數,m表示共有多少數.
  19. void AllRange(char *pszStr, int k, int m)
  20. {
  21. if (k == m)
  22. {
  23. static int s_i = 1;
  24. printf(" 第%3d個排列\t%s\n", s_i++, pszStr);
  25. }
  26. else
  27. {
  28. for (int i = k; i <= m; i++) //第i個數分別與它後面的數字交換就能得到新的排列
  29. {
  30. if (IsSwap(pszStr, k, i))
  31. {
  32. Swap(pszStr + k, pszStr + i);
  33. AllRange(pszStr, k + 1, m);
  34. Swap(pszStr + k, pszStr + i);
  35. }
  36. }
  37. }
  38. }
  39. void Foo(char *pszStr)
  40. {
  41. AllRange(pszStr, 0, strlen(pszStr) - 1);
  42. }
  43. int main()
  44. {
  45. printf(" 去重全排列的遞迴實現\n");
  46. printf(" --by MoreWindows( http://blog.csdn.net/MoreWindows )--\n\n");
  47. char szTextStr[] = "122";
  48. printf("%s的全排列如下:\n", szTextStr);
  49. Foo(szTextStr);
  50. return 0;
  51. }

執行結果如下:

 

OK,到現在我們已經能熟練寫出遞迴的方法了,並且考慮了字串中的重複資料可能引發的重複數列問題。那麼如何使用非遞迴的方法來得到全排列了?

三.全排列的非遞迴實現

要考慮全排列的非遞迴實現,先來考慮如何計算字串的下一個排列。如"1234"的下一個排列就是"1243"。只要對字串反覆求出下一個排列,全排列的也就迎刃而解了。

如何計算字串的下一個排列了?來考慮"926520"這個字串,我們從後向前找第一雙相鄰的遞增數字,"20"、"52"都是非遞增的,"26 "即滿足要求,稱前一個數字2為替換數,替換數的下標稱為替換點,再從後面找一個比替換數大的最小數(這個數必然存在),0、2都不行,5可以,將5和2交換得到"956220",然後再將替換點後的字串"6220"顛倒即得到"950226"。

對於像"4321"這種已經是最“大”的排列,採用STL中的處理方法,將字串整個顛倒得到最“小”的排列"1234"並返回false。

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

  1. //全排列的非遞迴實現
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. void Swap(char *a, char *b)
  6. {
  7. char t = *a;
  8. *a = *b;
  9. *b = t;
  10. }
  11. //反轉區間
  12. void Reverse(char *a, char *b)
  13. {
  14. while (a < b)
  15. Swap(a++, b--);
  16. }
  17. //下一個排列
  18. bool Next_permutation(char a[])
  19. {
  20. char *pEnd = a + strlen(a);
  21. if (a == pEnd)
  22. return false;
  23. char *p, *q, *pFind;
  24. pEnd--;
  25. p = pEnd;
  26. while (p != a)
  27. {
  28. q = p;
  29. --p;
  30. if (*p < *q) //找降序的相鄰2數,前一個數即替換數
  31. {
  32. //從後向前找比替換點大的第一個數
  33. pFind = pEnd;
  34. while (*pFind <= *p)
  35. --pFind;
  36. //替換
  37. Swap(pFind, p);
  38. //替換點後的數全部反轉
  39. Reverse(q, pEnd);
  40. return true;
  41. }
  42. }
  43. Reverse(p, pEnd);//如果沒有下一個排列,全部反轉後返回true
  44. return false;
  45. }
  46. int QsortCmp(const void *pa, const void *pb)
  47. {
  48. return *(char*)pa - *(char*)pb;
  49. }
  50. int main()
  51. {
  52. printf(" 全排列的非遞迴實現\n");
  53. printf(" --by MoreWindows( http://blog.csdn.net/MoreWindows )--\n\n");
  54. char szTextStr[] = "abc";
  55. printf("%s的全排列如下:\n", szTextStr);
  56. //加上排序
  57. qsort(szTextStr, strlen(szTextStr), sizeof(szTextStr[0]), QsortCmp);
  58. int i = 1;
  59. do{
  60. printf("第%3d個排列\t%s\n", i++, szTextStr);
  61. }while (Next_permutation(szTextStr));
  62. return 0;
  63. }

測試一下,結果如下所示:

將字串改成"cba"會輸出:

至此我們已經運用了遞迴與非遞迴的方法解決了全排列問題,總結一下就是:

1.全排列就是從第一個數字起每個數分別與它後面的數字交換。

2.去重的全排列就是從第一個數字起每個數分別與它後面非重複出現的數字交換。

3.全排列的非遞迴就是由後向前找替換數替換點,然後由後向前找第一個比替換數大的數與替換數交換,最後顛倒替換點後的所有資料。

如果覺得本文對您有幫助,請點選‘頂’支援一下,您的支援是我寫作最大的動力,謝謝。