全排列和组合问题
2014-09-05 09:19
190 查看
字符串的全排列
原理:每个字符不断与它后边不重复的字符交换,如果当前指向的字符和在前边已经交换过了,就不再进行交换。
要考虑全排列的非递归实现,先来考虑如何计算字符串的下一个排列。如"1234"的下一个排列就是"1243"。只要对字符串反复求出下一个排列,全排列的也就迎刃而解了。
如何计算字符串的下一个排列了?来考虑"926520"这个字符串,我们从后向前找第一双相邻的递增数字,"20"、"52"都是非递增的,"26 "即满足要求,称前一个数字2为替换数,替换数的下标称为替换点,再从后面找一个比替换数大的最小数(这个数必然存在),0、2都不行,5可以,将5和2交换得到"956220",然后再将替换点后的字符串"6220"颠倒即得到"950226"。
对于像“4321”这种已经是最“大”的排列,采用STL中的处理方法,将字符串整个颠倒得到最“小”的排列"1234"并返回false。
这样,只要一个循环再加上计算字符串下一个排列的函数就可以轻松的实现非递归的全排列算法。按上面思路并参考STL中的实现源码,不难写成一份质量较高的代码。值得注意的是在循环前要对字符串排序下,可以自己写快速排序的代码(请参阅《白话经典算法之六 快速排序 快速搞定》),也可以直接使用VC库中的快速排序函数(请参阅《使用VC库函数中的快速排序函数》)。下面列出完整代码:
[cpp] view
plaincopy
#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、全排列的非递归就是由后向前找替换数和替换点,然后由后向前找第一个比替换数大的数与替换数交换,最后颠倒替换点后的所有数据。
2.字符串的组合问题
可以分为把第一个加进去和不把第一个加进去两种情况
题目:输入一个字符串,输出该字符串中字符的所有组合。举个例子,如果输入abc,它的组合有a、b、c、ab、ac、bc、abc。
上面我们详细讨论了如何用递归的思路求字符串的排列。同样,本题也可以用递归的思路来求字符串的组合。
假设我们想在长度为n的字符串中求m个字符的组合。我们先从头扫描字符串的第一个字符。针对第一个字符,我们有两种选择:第一是把这个字符放到组合中去,接下来我们需要在剩下的n-1个字符中选取m-1个字符;第二是不把这个字符放到组合中去,接下来我们需要在剩下的n-1个字符中选择m个字符。这两种选择都很容易用递归实现。下面是这种思路的参考代码:
原理:每个字符不断与它后边不重复的字符交换,如果当前指向的字符和在前边已经交换过了,就不再进行交换。
#include<iostream> using namespace std; //在[nBegin,nEnd)区间中是否有符与下标为pEnd的字符相等 bool isSwap(char* pBegin,char*pEnd) { for(char *p =pBegin;p<pEnd;p++) { if(*p == *pEnd) return false; } return true; } void Permutation(char *arr,char* pBegin) { if(*pBegin == '\0') cout << arr <<endl; else { for(char* p= pBegin;*p!='\0';p++)//第pBegin个数分别与它后面的数字交换就能得到新的排列 { if(isSwap(pBegin,p)) { char temp =*pBegin; *pBegin = *p; *p = temp; Permutation(arr,pBegin + 1); temp =*p; *p = *pBegin; *pBegin = temp; } } } } void Permutation(char *arr) { if(arr == NULL) return; Permutation(arr,arr); } int _tmain(int argc, _TCHAR* argv[]) { char arr[]="aac"; Permutation(arr); system("pause"); return 0; }三、全排列的非递归实现
要考虑全排列的非递归实现,先来考虑如何计算字符串的下一个排列。如"1234"的下一个排列就是"1243"。只要对字符串反复求出下一个排列,全排列的也就迎刃而解了。
如何计算字符串的下一个排列了?来考虑"926520"这个字符串,我们从后向前找第一双相邻的递增数字,"20"、"52"都是非递增的,"26 "即满足要求,称前一个数字2为替换数,替换数的下标称为替换点,再从后面找一个比替换数大的最小数(这个数必然存在),0、2都不行,5可以,将5和2交换得到"956220",然后再将替换点后的字符串"6220"颠倒即得到"950226"。
对于像“4321”这种已经是最“大”的排列,采用STL中的处理方法,将字符串整个颠倒得到最“小”的排列"1234"并返回false。
这样,只要一个循环再加上计算字符串下一个排列的函数就可以轻松的实现非递归的全排列算法。按上面思路并参考STL中的实现源码,不难写成一份质量较高的代码。值得注意的是在循环前要对字符串排序下,可以自己写快速排序的代码(请参阅《白话经典算法之六 快速排序 快速搞定》),也可以直接使用VC库中的快速排序函数(请参阅《使用VC库函数中的快速排序函数》)。下面列出完整代码:
[cpp] view
plaincopy
#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、全排列的非递归就是由后向前找替换数和替换点,然后由后向前找第一个比替换数大的数与替换数交换,最后颠倒替换点后的所有数据。
2.字符串的组合问题
可以分为把第一个加进去和不把第一个加进去两种情况
题目:输入一个字符串,输出该字符串中字符的所有组合。举个例子,如果输入abc,它的组合有a、b、c、ab、ac、bc、abc。
上面我们详细讨论了如何用递归的思路求字符串的排列。同样,本题也可以用递归的思路来求字符串的组合。
假设我们想在长度为n的字符串中求m个字符的组合。我们先从头扫描字符串的第一个字符。针对第一个字符,我们有两种选择:第一是把这个字符放到组合中去,接下来我们需要在剩下的n-1个字符中选取m-1个字符;第二是不把这个字符放到组合中去,接下来我们需要在剩下的n-1个字符中选择m个字符。这两种选择都很容易用递归实现。下面是这种思路的参考代码:
#include<vector> #include<iostream> using namespace std; int _tmain(int argc, _TCHAR* argv[]) { return 0; } void Combination(char* string ,int number,vector<char> &result); void Combination(char* string) { if(string == NULL) return ; vector<char>result; int i,len = strlen(string);//字符串长度 for(i=1;i<=len;i++) Combination( string,i,result); } void Combination(char* arr ,int number,vector<char> &result) { if(arr == NULL) return ; if(number == 0)//遍历到最后,输出元素 { static int num = 1; cout <<"第" << num++ <<"个组合:" <<endl; vector<char>::iterator iter =result.begin(); for(;iter != result.end();++iter) cout << *iter ; cout << endl; return ; } if(*arr =='\0') return; result.push_back(*arr); Combination(arr+1,number-1,result);//把第一个字符加进去 result.pop_back(); Combination(arr+1,number,result);//不把第一个字符加进去 } int main(void) { char str[] = "abc"; Combination(str); system("pause"); return 0; }