一、全排列的递归实现
为方便起见,用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; }
另外一种写法:
//k表示当前选取到第几个数,m表示共有多少个数 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)-1); return 0; }如果字符串中有重复字符的话,上面的那个方法肯定不会符合要求的,因此现在要想办法来去掉重复的数列。
#include<iostream> using namespace std; #include<assert.h> //在[nBegin,nEnd)区间中是否有字符与下标为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,到现在我们已经能熟练写出递归的方法了,并且考虑了字符串中的重复数据可能引发的重复数列问题。那么如何使用非递归的方法来得到全排列了?
这样,只要一个循环再加上计算字符串下一个排列的函数就可以轻松的实现非递归的全排列算法。按上面思路并参考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; }至此我们已经运用了递归与非递归的方法解决了全排列问题,总结一下就是:
二、字符串的组合
题目:输入一个字符串,输出该字符串中字符的所有组合。举个例子,如果输入abc,它的组合有a、b、c、ab、ac、bc、abc。
上面我们详细讨论了如何用递归的思路求字符串的排列。同样,本题也可以用递归的思路来求字符串的组合。#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; }