字符串的排列及组合算法

一、字符串的排列

问题1:输入一个字符串(无重复字符),打印出该字符串中字符的所有排列。

例如:输入字符串abc,则打印出由字符a、b、c所能排列出来的所有字符串abc、acb、bac、bca、cab和cba

思路:可以把一个字符串看成两部分组成,第一部分为它的第一个字符,第二部分为后面所有的字符。

我们求整个字符串的全排列,可以看成两步。首先,求所有可能出现在第一个位置的字符,即把第一个字符和后面的所有的字符交换;第二步,固定第一个字符,求后面所有字符的排列。这个时候我们仍把后面的所有字符分成两部分:第一个字符和该字符后面的所有字符。然后把第一个字符逐一和它后面的字符交换。

很明显这是一个递归的过程,代码如下:

#include<iostream>
#include<cassert>
using namespace std;

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;
}
 

如果这一题要求按照字典序来输出结果,代码如下:

#include<iostream>
#include<cassert>
#include<algorithm>
using namespace std;


int cmp(const void *a,const void *b)  
{  
	return int(*(char *)a - *(char *)b);  
} 


void Permutation(char* pStr, char* pBegin)
{
	assert(pStr && pBegin);

	if(*pBegin == '\0')
		printf("%s\n",pStr);
	else
	{
		for(char* pCh = pBegin; *pCh != '\0'; pCh++)
		{
			qsort(pCh, strlen(pCh),sizeof(char),cmp);
			swap(*pBegin,*pCh);
			Permutation(pStr, pBegin+1);
			swap(*pBegin,*pCh);
		}
	}
}

int main(void)
{
	char str[] = "abc";
	Permutation(str,str);
	return 0;
}

 

问题2:输入一个字符串(有重复字符),打印出该字符串中字符的所有排列。

解法:由于全排列就是从第一个数字起每个数分别与它后面的数字交换。我们先尝试加个这样的判断——如果一个数与后面的数字相同那么这二个数就不交换了。如122,第一个数与后面交换得212、221。然后122中第二数就不用与第三个数交换了,但对212,它第二个数与第三个数是不相同的,交换之后得到221。与由122中第一个数与第三个数交换所得的221重复了。所以这个方法不行。

换种思维,对122,第一个数1与第二个数2交换得到212,然后考虑第一个数1与第三个数2交换,此时由于第三个数等于第二个数,所以第一个数不再与第三个数交换。再考虑212,它的第二个数与第三个数交换可以得到解决221。此时全排列生成完毕。这样我们也得到了在全排列中去掉重复的规则——去重的全排列就是从第一个字符起每个字符分别与它后面非重复出现的字符交换。下面给出完整代码:

#include<iostream>
#include<cassert>
using namespace std;

//在[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;
}
这里如果需要字典序输出,亦可按照上面样例修改程序。

实现递归后,我们再来考虑下非递归实现。非递归实现有很多种办法,可以参考全排列生成算法,这篇博文里重点说明了字典序法,在这里我将主题思路及程序实现照搬过来。

思路:

设P是1~n的一个全排列:P=p(1)p(2)......p(n-1)p(n) = p(1)p(2)......p(j-1)p(j)p(j+1)......p(k-1)p(k)p(k+1)......p(n-1)p(n)

1)从排列的右端开始,找出第一个比右边数字小的数字的序号j(j从左端开始计算),即 j=max{i|p(i)<p(i+1)}
2)在p(j)的右边的数字中,找出所有比p(j)大的数中最小的数字p(k),即 k=max{i|p(i)>p(j)}(右边的数从右至左是递增的,因此k是所有大于pj的数字中序号最大者)
3)对换p(j),p(k)
4)再将p(j+1)......p(k-1)p(k)p(k+1)......p(n)倒转得到排列p'=p(1)p(2).....p(j-1)p(j)p(n).....p(k+1)p(k)p(k-1).....p(j+1),这就是排列P的下一个排列。

#include<algorithm>
#include<cstring>
#include<cassert>
using namespace std;

//反转区间
void Reverse(char* pBegin , char* pEnd)
{
	while(pBegin < pEnd)
		swap(*pBegin++ , *pEnd--);
}

//下一个排列
bool Next_permutation(char str[])
{
	assert(str);
	char *front , *rear , *pFind;
	char *pEnd = str + strlen(str) - 1;
	if(str == pEnd)
		return false;
	front = pEnd;
	while(front != str)
	{
		rear = front;
		front--;
		if(*front < *rear)  //找降序的相邻两数,前一个数即替换数  
		{
			//从后向前找比替换点大的第一个数
			pFind = pEnd;
			while(*pFind < *rear)
				--pFind;
			swap(*front , *pFind);
			//替换点后的数全部反转
			Reverse(rear , pEnd);
			return true;
		}
	}
	Reverse(str , pEnd);   //如果没有下一个排列,全部反转后返回false   
	return false;
}

int cmp(const void *a,const void *b)
{
	return int(*(char *)a - *(char *)b);
}

int main(void)
{
	char str[] = "745328916";
	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

思路:假设我们想在长度为n的字符串中求m个字符的组合。我们先从头扫描字符串的第一个字符。针对第一个字符,我们有两种选择:第一是把这个字符放到组合中去,接下来我们需要在剩下的n-1个字符中选取m-1个字符;第二是不把这个字符放到组合中去,接下来我们需要在剩下的n-1个字符中选择m个字符。这两种选择都很容易用递归实现。下面是这种思路的参考代码:

#include<iostream>
#include<vector>
#include<cstring>
#include<cassert>
using namespace std;


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;
}
求组合还有另外一种方法,输入字符串abc,则其可能的组合数为C(3,1)+C(3,2)+C(3,3)。 对于元素个数为 n 的集合,可以使用 n 为来表示每一个元素,为 1 表示该元素被选中,为 0 表示该元素未被选中。那么,计算组合 C(n, k)  就相当于计算出 n 位数中有 k 1 位的所有数,每一个计算出的数就表示一个选中的组合。

假设有n个元素的集合,组合数为C(n,1)+C(n,2)+C(n,3)+......+C(n,n-1)+C(n,n) = 2^n,所以我们知道n个元素的集合其组合数总数为2^n个,那么我们直接用[0, 2^n)范围内的所有整数来取组合,取组合的时候可以根据当前数的二进制位中哪些为1,为1代表选取该位对应字符添加到组合中。例如:输入字符串为abcde,我们要用5位来取组合。 2 = 00010 ,第二位为1,则组合为b。19=10011,此时对应的组合为abe。参考程序如下:

#include<iostream>
using namespace std;

int a[] = {1,3,5,4,6};
char str[] = "abcde";

void print_subset(int n , int s)
{
	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(strlen(str));
	return 0;
}

 

参考博客:http://blog.csdn.net/hackbuteer1/article/details/7462447


                
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值