53.字符串的排列(字符串)。
題目:輸入一個字符串,打印出該字符串中字符的所有排列。
例如輸入字符串 abc,則輸出由字符 a、b、c 所能排列出來的所有字符串
abc、acb、bac、bca、cab 和 cba。
這道題花了我一天,要好好總結!
思路:這道題目感覺有些難,主要是字符串中的字符可能會有重復。我的想法是把一共有多少種字符和每種字符出現的次數統計出來,每個位置對這些字符變量,下一個位置的可用字符減小,再遍歷。
/* 53.字符串的排列(字符串)。 題目:輸入一個字符串,打印出該字符串中字符的所有排列。 例如輸入字符串 abc,則輸出由字符 a、b、c 所能排列出來的所有字符串 abc、acb、bac、bca、cab 和 cba。 */ #include <stdio.h> #include <string.h> #include <stdlib.h> char A[128][2] = {0}; //統計字符串中每種字符出現的次數 char ANS[10000]; //可能的輸出 int N; //字符種類數 int LEN; //輸入字符串長度 int getTimes(char * in) //統計字符個數 以及 每個字符出現的次數 { int len = strlen(in); bool B[100] = {0}; int n = 0; for(int i = 0; in[i] != '\0'; i++) { if(B[i] == false) { for(int j = i; in[j] != '\0'; j++) { if(in[j] == in[i]) { A[n][0] = in[i]; A[n][1]++; B[j] = true; } } n++; } } return n; } void traverse(int len) //遞歸求解 每個位置遍歷選用A中可用次數大於0的的字符 { if(len == 0) { for(int i = 0; i < LEN; i++) { printf("%c ", ANS[i]); } printf("\n"); return; } for(int i = 0; i < N; i++) { if(A[i][1] != 0) //如果不等於0 說明該字符還可以使用 { ANS[LEN - len] = A[i][0]; //把當前字符存在答案中 A[i][1]--; //當前字符可用次數減1 traverse(len - 1); //求下一個位置的字符 A[i][1]++; //恢復當前字符的可使用次數 } } } void getall(char * in) { LEN = strlen(in); N = getTimes(in); traverse(LEN); } int main() { char in[100] = "abcc"; getall(in); return 0; }
寫完后就覺得自己寫的不好,好多全局變量,看着難受。
網上看到了很好的解答:http://blog.csdn.net/hackbuteer1/article/details/7462447
給出了遞歸和非遞歸的思路。我看完后也仿照着思路重寫了一遍,又照着修改了下。
二、去掉重復的全排列的遞歸實現
由於全排列就是從第一個數字起每個數分別與它后面的數字交換。我們先嘗試加個這樣的判斷——如果一個數與后面的數字相同那么這二個數就不交換了。如122,第一個數與后面交換得212、221。然后122中第二數就不用與第三個數交換了,但對212,它第二個數與第三個數是不相同的,交換之后得到221。與由122中第一個數與第三個數交換所得的221重復了。所以這個方法不行。
換種思維,對122,第一個數1與第二個數2交換得到212,然后考慮第一個數1與第三個數2交換,此時由於第三個數等於第二個數,所以第一個數不再與第三個數交換。再考慮212,它的第二個數與第三個數交換可以得到解決221。此時全排列生成完畢。
這樣我們也得到了在全排列中去掉重復的規則——去重的全排列就是從第一個數字起每個數分別與它后面非重復出現的數字交換。
三、全排列的非遞歸實現
要考慮全排列的非遞歸實現,先來考慮如何計算字符串的下一個排列。如"1234"的下一個排列就是"1243"。只要對字符串反復求出下一個排列,全排列的也就迎刃而解了。
如何計算字符串的下一個排列了?來考慮"926520"這個字符串,我們從后向前找第一雙相鄰的遞增數字,"20"、"52"都是非遞增的,"26 "即滿足要求,稱前一個數字2為替換數,替換數的下標稱為替換點,再從后面找一個比替換數大的最小數(這個數必然存在),0、2都不行,5可以,將5和2交換得到"956220",然后再將替換點后的字符串"6220"顛倒即得到"950226"。
對於像“4321”這種已經是最“大”的排列,采用STL中的處理方法,將字符串整個顛倒得到最“小”的排列"1234"並返回false。
這樣,只要一個循環再加上計算字符串下一個排列的函數就可以輕松的實現非遞歸的全排列算法。按上面思路並參考STL中的實現源碼,不難寫成一份質量較高的代碼。值得注意的是在循環前要對字符串排序下,可以自己寫快速排序的代碼
/* http://blog.csdn.net/hackbuteer1/article/details/7462447 在上面的網址上看了答案后 發現自己寫的代碼實在是太挫了,有很多不必要的全局變量。還有輸入字符中一共有幾種字符,每種字符的出現次數其實可以不用具體 求出來的。直接比較判斷就好了。 網上還給出了非遞歸算法,找到了從小到大輸出所有可能組合的方法,非常值得學習。 */ //遞歸 #include<stdio.h> #include<string.h> #include <algorithm> using namespace std; bool isswap(char * pBegin, char * pNow) { for(char *p = pBegin; p < pNow; p++) { if(*p == *pNow) return false; } return true; } void traverse(char * pStr,char * pBegin) { if(/**pBegin == '\0'*/strlen(pBegin) == 1) //兩種判斷方式都可以 { static int n = 0; printf("%d: %s\n", ++n, pStr); } else { for(char *p = pBegin; *p != '\0'; p++) { if(isswap(pBegin, p)) { swap(*pBegin, *p); //注意swap應該是 數值 而不是指針 traverse(pStr, pBegin + 1); swap(*p, *pBegin); } } } } //非遞歸 int cmp(const void * a,const void * b) { return *((char *)a) - *((char *)b); } void reverse(char * pBegin, char * pEnd) { while(pBegin < pEnd) { swap(*pBegin++, *pEnd--); } } bool nonrtraverse(char * pStr) { int l = strlen(pStr); char * pEnd = pStr + l - 1; //定位最后一個字符 if(pStr == pEnd) //注意對只有一個字符的處理 return false; for(char *p = pEnd; p != pStr; p--) { char * q = p - 1; if(*q < *p) { char* pFind = pEnd; while(*pFind <= *q) //注意這里,找到第一個比交換點數大的數 --pFind; swap(*q, *pFind); reverse(p, pEnd); return true; } } reverse(pStr, pStr +l - 1); return false; } int main() { char str[30] = "1224"; traverse(str, str); qsort(str, strlen(str)-1, sizeof(char), cmp); int i = 0; do { printf("%d:%s\n", ++i, str); } while(nonrtraverse(str)); return 0; }