字符串的排列及組合算法

一、字符串的排列

問題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位數中有k1位的所有數,每一個計算出的數就表示一個選中的組合。

假設有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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章