首先來看看題目是如何要求的(百度迅雷校招筆試題)。
(轉載:http://blog.csdn.net/hackbuteer1/article/details/7462447)
用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;
#include<assert.h>void Permutation(char* pStr, char* pBegin)
{assert(pStr && pBegin);if(*pBegin == '\0')printf("%s\n",pStr);else{for(char* pCh = pBegin; *pCh != '\0'; pCh++){swap(*pBegin,*pCh);Permutation(pStr, pBegin+1);swap(*pBegin,*pCh);}}
}int main(void)
{char str[] = "abc";Permutation(str,str);return 0;
}
另外一種寫法:
</pre><pre name="code" class="cpp"><pre name="code" class="cpp">//k表示當前選取到第幾個數,m表示共有多少個數
#include <iostream>
#include <cstring>
#include <cassert>
using namespace std;
void Permutation(char* pStr,int k,int m)
{assert(pStr);if(k == m){static int num = 1; //局部靜態變量,用來統計全排列的個數printf("第%d個排列\t%s\n",num++,pStr);}else{for(int i = k; i < m; i++){swap(*(pStr+k),*(pStr+i));Permutation(pStr, k + 1 , m);swap(*(pStr+k),*(pStr+i));}}
}int main(void)
{char str[] = "abc";Permutation(str , 0 , strlen(str));return 0;
}
如果字符串中有重復字符的話,上面的那個方法肯定不會符合要求的,因此現在要想辦法來去掉重復的數列。
二、去掉重復的全排列的遞歸實現
由于全排列就是從第一個數字起每個數分別與它后面的數字交換。我們先嘗試加個這樣的判斷——如果一個數與后面的數字相同那么這二個數就不交換了。如122,第一個數與后面交換得212、221。然后122中第二數就不用與第三個數交換了,但對212,它第二個數與第三個數是不相同的,交換之后得到221。與由122中第一個數與第三個數交換所得的221重復了。所以這個方法不行。
換種思維,對122,第一個數1與第二個數2交換得到212,然后考慮第一個數1與第三個數2交換,此時由于第三個數等于第二個數,所以第一個數不再與第三個數交換。再考慮212,它的第二個數與第三個數交換可以得到解決221。此時全排列生成完畢。
這樣我們也得到了在全排列中去掉重復的規則——去重的全排列就是從第一個數字起每個數分別與它后面非重復出現的數字交換。下面給出完整代碼:
#include<iostream>using namespace std;#include<assert.h>//在[nBegin,nEnd)區間中是否有字符與下標為pEnd的字符相等,也就是在pEnd之前是否有與之相等的數bool IsSwap(char* pBegin , char* pEnd){char *p;for(p = pBegin ; p < pEnd ; p++){if(*p == *pEnd)return false;}return true;}void Permutation(char* pStr , char *pBegin){assert(pStr);if(*pBegin == '\0'){static int num = 1; ?//局部靜態變量,用來統計全排列的個數printf("第%d個排列\t%s\n",num++,pStr);}else{for(char *pCh = pBegin; *pCh != '\0'; pCh++) ? //第pBegin個數分別與它后面的數字交換就能得到新的排列 ??{if(IsSwap(pBegin , pCh)){swap(*pBegin , *pCh);Permutation(pStr , pBegin + 1);swap(*pBegin , *pCh);}}}}int main(void){char str[] = "baa";Permutation(str , str);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<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
#include<assert.h>//反轉區間
void Reverse(char* pBegin , char* pEnd)
{while(pBegin < pEnd)swap(*pBegin++ , *pEnd--);
}
//下一個排列
bool Next_permutation(char a[])
{assert(a);char *p , *q , *pFind;char *pEnd = a + strlen(a) - 1;if(a == pEnd)return false;p = pEnd;while(p != a){q = p;p--;if(*p < *q) //找降序的相鄰2數,前一個數即替換數 {//從后向前找比替換點大的第一個數pFind = pEnd;while(*pFind < *p)--pFind;swap(*p , *pFind);//替換點后的數全部反轉Reverse(q , pEnd);return true;}}Reverse(a , pEnd); //如果沒有下一個排列,全部反轉后返回false return false;
}int cmp(const void *a,const void *b)
{return int(*(char *)a - *(char *)b);
}
int main(void)
{char str[] = "bac";int num = 1;qsort(str , strlen(str),sizeof(char),cmp);do{printf("第%d個排列\t%s\n",num++,str); }while(Next_permutation(str));return 0;
}
至此我們已經運用了遞歸與非遞歸的方法解決了全排列問題,總結一下就是:
1、全排列就是從第一個數字起每個數分別與它后面的數字交換。
2、去重的全排列就是從第一個數字起每個數分別與它后面非重復出現的數字交換。
3、全排列的非遞歸就是由后向前找替換數和替換點,然后由后向前找第一個比替換數大的數與替換數交換,最后顛倒替換點后的所有數據。
二、字符串的組合
題目:輸入一個字符串,輸出該字符串中字符的所有組合。舉個例子,如果輸入abc,它的組合有a、b、c、ab、ac、bc、abc。
上面我們詳細討論了如何用遞歸的思路求字符串的排列。同樣,本題也可以用遞歸的思路來求字符串的組合。假設我們想在長度為n的字符串中求m個字符的組合。我們先從頭掃描字符串的第一個字符。針對第一個字符,我們有兩種選擇:第一是把這個字符放到組合中去,接下來我們需要在剩下的n-1個字符中選取m-1個字符;第二是不把這個字符放到組合中去,接下來我們需要在剩下的n-1個字符中選擇m個字符。這兩種選擇都很容易用遞歸實現。下面是這種思路的參考代碼:
#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
#include<assert.h>void Combination(char *string ,int number,vector<char> &result);void Combination(char *string)
{assert(string != NULL);vector<char> result;int i , length = strlen(string);for(i = 1 ; i <= length ; ++i)Combination(string , i ,result);
}void Combination(char *string ,int number , vector<char> &result)
{assert(string != NULL);if(number == 0){static int num = 1;printf("第%d個組合\t",num++);vector<char>::iterator iter = result.begin();for( ; iter != result.end() ; ++iter)printf("%c",*iter);printf("\n");return ;}if(*string == '\0')return ;result.push_back(*string);Combination(string + 1 , number - 1 , result);result.pop_back();Combination(string + 1 , number , result);
}int main(void)
{char str[] = "abc";Combination(str);return 0;
}
由于組合可以是1個字符的組合,2個字符的字符……一直到n個字符的組合,因此在函數void Combination(char* string),我們需要一個for循環。另外,我們用一個vector來存放選擇放進組合里的字符。
方法二:用位運算來實現求組合
#include<iostream>
using namespace std;int a[] = {1,3,5,4,6};
char str[] = "abcde";void print_subset(int n , int s)
{printf("{");for(int i = 0 ; i < n ; ++i){if( s&(1<<i) ) // 判斷s的二進制中哪些位為1,即代表取某一位printf("%c ",str[i]); //或者a[i]}printf("}\n");
}void subset(int n)
{for(int i= 0 ; i < (1<<n) ; ++i){print_subset(n,i);}
}int main(void)
{subset(5);return 0;
}
字符串全排列擴展----八皇后問題題目:在8×8的國際象棋上擺放八個皇后,使其不能相互攻擊,即任意兩個皇后不得處在同一行、同一列或者同一對角斜線上。下圖中的每個黑色格子表示一個皇后,這就是一種符合條件的擺放方法。請求出總共有多少種擺法。這就是有名的八皇后問題。解決這個問題通常需要用遞歸,而遞歸對編程能力的要求比較高。因此有不少面試官青睞這個題目,用來考察應聘者的分析復雜問題的能力以及編程的能力。由于八個皇后的任意兩個不能處在同一行,那么這肯定是每一個皇后占據一行。于是我們可以定義一個數組ColumnIndex[8],數組中第i個數字表示位于第i行的皇后的列號。先把ColumnIndex的八個數字分別用0-7初始化,接下來我們要做的事情就是對數組ColumnIndex做全排列。由于我們是用不同的數字初始化數組中的數字,因此任意兩個皇后肯定不同列。我們只需要判斷得到的每一個排列對應的八個皇后是不是在同一對角斜線上,也就是數組的兩個下標i和j,是不是i-j==ColumnIndex[i]-Column[j]或者j-i==ColumnIndex[i]-ColumnIndex[j]。關于排列的詳細討論,詳見上面的講解。
接下來就是寫代碼了。思路想清楚之后,編碼并不是很難的事情。下面是一段參考代碼:
#include<iostream>
using namespace std;int g_number = 0;
void Permutation(int * , int , int );
void Print(int * , int );void EightQueen( )
{const int queens = 8;int ColumnIndex[queens];for(int i = 0 ; i < queens ; ++i)ColumnIndex[i] = i; //初始化Permutation(ColumnIndex , queens , 0);
}bool Check(int ColumnIndex[] , int length)
{int i,j;for(i = 0 ; i < length; ++i){for(j = i + 1 ; j < length; ++j){if( i - j == ColumnIndex[i] - ColumnIndex[j] || j - i == ColumnIndex[i] - ColumnIndex[j]) //在正、副對角線上return false;}}return true;
}
void Permutation(int ColumnIndex[] , int length , int index)
{if(index == length){if( Check(ColumnIndex , length) ) //檢測棋盤當前的狀態是否合法{++g_number;Print(ColumnIndex , length);}}else{for(int i = index ; i < length; ++i) //全排列{swap(ColumnIndex[index] , ColumnIndex[i]);Permutation(ColumnIndex , length , index + 1);swap(ColumnIndex[index] , ColumnIndex[i]);}}
}void Print(int ColumnIndex[] , int length)
{printf("%d\n",g_number);for(int i = 0 ; i < length; ++i)printf("%d ",ColumnIndex[i]);printf("\n");
}int main(void)
{EightQueen();return 0;
}
轉載:http://zhedahht.blog.163.co
題目:輸入兩個整數n和m,從數列1,2,3...n中隨意取幾個數,使其和等于m,要求列出所有的組合。
#include <iostream>
#include <list>
using namespace std;
list<int> list1;
void find_factor(int sum,int n)
{//遞歸出口if(n<=0||sum<=0)return;//輸出找到的數if(sum==n){list1.reverse();for(list<int>::iterator iter=list1.begin();iter!=list1.end();iter++)cout<<*iter<<"+";cout<<n<<endl;list1.reverse();}list1.push_front(n);find_factor(sum-n,n-1);//n放在里面list1.pop_front();find_factor(sum,n-1);//n不放在里面
}int main(void)
{int sum,n;cin>>sum>>n;cout<<"所有可能的序列,如下:"<<endl;find_factor(sum,n);return 0;
}