排列与组合

一:全排列

描述:给定一个字符序列或者一个整形序列,输出其所有可能的排列结果。

分析:排列这个问题在高中数学教材中已经被描述的相当详尽了,简单来说就是有N个位置,然后有N个不同的物体,你需要将这N个物体放到这N个位置上,问你有多少种放的方式,并且输出所有的摆放情况。

如果我们在编程中遇到的一个问题是,求一个数组的全排列;假设这个数组的长度为N,则这个数组的N个存储单元就是上面提到的N个位置,数组中的元素就是N个物体。

还是回到高中时解题的思路:在将N个不同的物体放置到N个位置中时,第一个物体可以有N个选择;在第一个物体选中一个位置的情况下,第二个物体有N-1种选择;...;在前N-1个物体已将选中的情况下,第N个物体只有一种选择。但是在所有的排列情况中,每一个物体都在所有的位置处存放过。所以可以通过模拟这个放置物体的过程来形成所有可能的全排列。一个递归代码如下:

  5 #define SWAP(a,b,temp) do{(temp) = (a); (a) = (b); (b) = (temp);}while(false)
  6 void arrange(int level, int length, char* str)
  7 {
  8     if(level == length)
  9     {
 10         //确保输入字符串是以'\0'结尾
 11         cout<<str<<endl;
 12         return;
 13     }
 14     else
 15     {   
 16         char ch;
 17         for(int i = level; i < length; i++)
 18         {   
 19             //每一个数组元素都与其后的每一个元素进行交换,这样可保证每个元素都在每一个存储位置待过
 20             SWAP(str[level], str[i], ch);
 21             arrange(level + 1, length, str);
 22             SWAP(str[level], str[i], ch);
 23         }
 24     }
 25 }
以上的过程就是完全模拟的“把N个小球放到N个盒子里”的过程。

不过以上的代码仅仅适用于“不同的小球”,即数组里的每个元素都是不同的,不然会出现重复的情况。

对于上述方法,由于相同的排列方式的出现并不是紧邻的(程序执行流上的相邻),所以要判断一个排列是否已经出现过的代价较高(可以想到的一个方案是将所有的已经声生成的排列结果存储起来,可以使用字典树保证每次查找时间仅为字符串长度,然后每次生成一个新的排列结果时,就去依次比较看是否已经出现),完全可以舍弃掉这个方法了。

非递归的全排列实现方法:

通过观察问题规模较为简单的实例,然后找到问题的一般解决方法,这在算法设计中是很有用的。我们试着手动列出所有的“abcd”的全排列:

abcd, abdc, acbd, acdb, adbc, adcb, bacd, badc, bcad, bcda, bdac,bdca,

cabd, cadb, cbad, cbda, cdab, cdba, dabc, dacb, dbac, dbca, dcab,dcba

上面列出了“abcd”四个字符的全排列,我们可以发现两个特征:

1.所有这些特征是按字典序排列的

2.相邻两个排列之间的变化,即由前一个排列如何变化到下一个排列

这里主要讲第二个特征,应为他是我们解题的指导。

1.对初始序列进行排序,排序后的结果作为第一个排列,此时序列为L[0,N-1]

2.对当前的排列,从后向前扫描,当找到第一个递减点时停止,设递减点索引为i。

3.将已扫描过的元素的最小且大于L[i-1]的元素与L[i-1]交换(j>=i&&j<=N-1&&L[j]=min(L[0,N-1])&&L[j]>L[i-1]),并且再对已经扫描过的范围L[i,N-1]进行排序

如abcd 到 abdc的变化:从后向前扫描,定位到第一个递减点d,则交换d与c。

算法代码如下:

#define SWAP(a, b, temp) do{                      \
				(temp) = (a);     \
				(a)    = (b);     \
				(b)    = (temp);  \
				}while(0)
int partition(char* str, int low, int high)
{
	int i = low;
	int j = high + 1;
	int temp;
	for(;;)
	{
		do
		{
			i++;
		}while(i <= high && str[i] < str[low]);
		do
		{
			j--;
		}while(str[j] > str[low]);
		if(i > j)
		{
			break;
		}
		SWAP(str[i], str[j], temp);
	}
	SWAP(str[j], str[low], temp);
	return j;
}
void quick_sort(char* str, int low, int high)
{
	int mid;
	if(low < high)
	{
		mid = partition(str, low, high);
		quick_sort(str, low, mid - 1);
		quick_sort(str, mid + 1, high);
	}
}
void reverse(char* str, int low, int high)
{
	char temp;
	while(low < high)
	{
		SWAP(str[low], str[high], temp);
		low++;
		high--;
	}
}
void arrange(char* str)
{
	//sort(str);
	int count = 0;
	if(str == NULL)
	{
		return;
	}
	char temp;
	int length = strlen(str);
	quick_sort(str, 0, length - 1);
	int i;
	for(;;)
	{
		count++;
		printf("%s\n", str);
		//注意<=与<的区别,重复字符的情况
		for(i = length - 2; i >= 0 && str[i] <span style="color:#ff0000;">>=</span> str[i + 1]; i--);
		if(i == -1)
		{
			break;
		}
		else if(i == length - 2)
		{
			SWAP(str[i], str[i + 1], temp);
		}
		else
		{
			int j = length - 1;
			//注意<=与<的区别,重复字符的情况
			while(str[j--] <span style="color:#ff0000;"><=</span> str[i]);
			j++;
			SWAP(str[i], str[j], temp);
			reverse(str, i + 1, length - 1);
		}
	}
	printf("arrange count is %d\n", count);
}
由于在这个解法中,所有的排列都是按照字典序依次生成的,所以就比较容易的实现对重复排列的抑制。见代码中红色的部分。

二.组合

组合问题的描述:给定N个物品,从中选出K个,要求输出所有的选择方案。

解题思路:对每一个物体,都有选中与不被选中两种状态。

f[n,m]表示从n个物品中选出m个物品的方案,对当前第一个物品如果我们选择了它,则问题变为f[n-1,m-1]如果我们没有选择它,则问题变成了f[n-1,m]。道理就是这样,现在我们可以写出一个递归的算法实现来输出所有可能的结果。

f[n,m] = f[n - 1, m - 1] + f[n - 1, m]

void print(int* pArray)
{
	int i = 0;
	for(i = 0; i < length; i++)
	{
		if(pArray[i] == 1)
		{
			printf("%c ",'a' + i);
		}
	}
	printf("\n");
}
void combination(int* pArray, int n, int m, int level)
{
	assert(n >= m);
	if(n == m)
	{
		int i;
		for(i = 0; i < m; i++)
		{
			pArray[level + i] = 1;
		}
		print(pArray);
		for(i = 0; i < m; i++)
		{
			pArray[level + i] = 0;
		}
	}
	else if(m == 0)
	{
		print(pArray);
	}
	else
	{
		pArray[level] = 1;
		combination(pArray, n - 1, m - 1, level + 1);
		pArray[level] = 0;
		combination(pArray, n - 1, m, level + 1);
	}
}


用排列解组合:

其实我们可以将组合问题转化成排列问题来求解,还是从N个物体中拿出K个,这相当于求一个长度为N的有K1N-K0的数组array[0-N-1]的全排列问题。Array[i]==1表示第i个元素被选择,Array[i]==0表示第i个元素不被选择。所以数组Array的全排列就对应了组合问题所有的选择情况。

注意:由组合问题转化而来的排列问题不能用本文提到的第一个排列算法,因为这是一个存在大量重复元素的排列问题(因为元素只有两种取值)。

所以我们可以直接使用第二种排列算法,下面再给出一个不同于第二种排列算法实现的代码,但原理相同。

void combination(int n, int m)
{
	char* pIndexArray = (char*)malloc(sizeof(char) * n);
	char temp;
	int i;
	for(i = 0; i < m; i++)
	{
		int j;
		for(j = 0; j < n; j++)
		{
			pIndexArray[j] = 1;
		}
		for(j = n - 1 - i; j >= m - i; j--)
		{
			pIndexArray[j] = 0;
		}
		print(pIndexArray, n);
		if(j == -1 || j == n - 1)
		{
			break;
		}
		for(; j >= 0; j--)
		{
			SWAP(pIndexArray[j], pIndexArray[j + 1], temp);
			//print
			print(pIndexArray, n);
		}
		if(m == n - 1)
		{
			break;
		}
	}
	free(pIndexArray);
}






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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值