全排列和全组合实现

转自:http://wuchong.me/blog/2014/07/28/permutation-and-combination-realize/

全排列和全组合实现

记得@老赵之前在微博上吐槽说,“有的人真是毫无长进,六年前某同事不会写程序输出全排列,昨天发邮件还是问我该怎么写,这时间浪费到我都看不下去了。” 那时候就很好奇全排列到底是什么东西,到底有多难?

今天复习的时候终于碰到这题了,结果果然自己太渣,看了好久都没明白,代码实现又是磕磕碰碰的。所以,就把它整理成笔记加深记忆,也希望能帮到和我一样的人。

全排列

所谓全排列,就是打印出字符串中所有字符的所有排列。例如输入字符串abc,则打印出 a、b、c 所能排列出来的所有字符串 abcacbbacbcacab 和 cba 。

一般最先想到的方法是暴力循环法,即对于每一位,遍历集合中可能的元素,如果在这一位之前出现过了该元素,跳过该元素。例如对于abc,第一位可以是 a 或 b 或 c 。当第一位为 a 时,第二位再遍历集合,发现 a 不行,因为前面已经出现 a 了,而 b 和 c 可以。当第二位为 b 时 , 再遍历集合,发现 a 和 b 都不行,c 可以。可以用递归或循环来实现,但是复杂度为  O(nn)  。有没有更优雅的解法呢。

首先考虑baccba这二个字符串是如何得出的。显然这二个都是abc中的 a 与后面两字符交换得到的。然后可以将abc的第二个字符和第三个字符交换得到acb。同理可以根据baccba来得bcacab

因此可以知道 全排列就是从第一个数字起每个数分别与它后面的数字交换,也可以得出这种解法每次得到的结果都是正确结果,所以复杂度为 O(n!)。找到这个规律后,递归的代码就很容易写出来了:

#include<stdio.h>
#include<string>

//交换两个字符
void Swap(char *a ,char *b)
{
	char temp = *a;
	*a = *b;
	*b = temp;
}

//递归全排列,start 为全排列开始的下标, length 为str数组的长度
void AllRange(char* str,int start,int length)
{
	if(start == length-1)
	{
		printf("%s\n",str);
	}
	else
	{
		for(int i=start;i<=length-1;i++)	
		{	//从下标为start的数开始,分别与它后面的数字交换
			Swap(&str[start],&str[i]); 
			AllRange(str,start+1,length);
			Swap(&str[start],&str[i]); 
		}
	}
}

void Permutation(char* str)
{
	if(str == NULL)
		return;

	AllRange(str,0,strlen(str));
}

void main()
{
	char str[] = "abc";
	Permutation(str);
}

应用演示实例:


public static void main(String[] args) {
<span style="white-space:pre">		</span>Scanner cin=new Scanner(System.in);
<span style="white-space:pre">		</span>while (cin.hasNext()) {
<span style="white-space:pre">			</span>int n=cin.nextInt();
<span style="white-space:pre">			</span>int K=cin.nextInt();
<span style="white-space:pre">			</span>int count=0;
<span style="white-space:pre">			</span>String[]p=new String[n];
<span style="white-space:pre">			</span>int[] cur=new int [n];
<span style="white-space:pre">			</span>for (int i = 0; i < n; i++) {
<span style="white-space:pre">				</span>p[i]=cin.next();
<span style="white-space:pre">				</span>cur[i]=i;
<span style="white-space:pre">			</span>}
<span style="white-space:pre">			</span>
<span style="white-space:pre">			</span>
<span style="white-space:pre">			</span>ArrayList<int[]>rec=new ArrayList<>();
<span style="white-space:pre">			</span>allRange(cur, 0, n, rec);
<span style="white-space:pre">			</span>
<span style="white-space:pre">			</span>for (int i = 0; i < rec.size(); i++) {
<span style="white-space:pre">				</span>int[] samp=rec.get(i);
<span style="white-space:pre">				</span>StringBuffer temp=new StringBuffer();
<span style="white-space:pre">				</span>for (int j = 0; j < samp.length; j++) {
<span style="white-space:pre">					</span>temp.append(p[samp[j]]);
<span style="white-space:pre">				</span>}
<span style="white-space:pre">				</span>//判断temp权值
<span style="white-space:pre">				</span>int v=value(temp);
<span style="white-space:pre">				</span>
<span style="white-space:pre">				</span>if (v>=K) {
<span style="white-space:pre">					</span>count++;
<span style="white-space:pre">				</span>}
<span style="white-space:pre">				</span>
<span style="white-space:pre">			</span>}
<span style="white-space:pre">			</span>
<span style="white-space:pre">			</span>System.out.println(count);
<span style="white-space:pre">			</span>
<span style="white-space:pre">			</span>
<span style="white-space:pre">			</span>
<span style="white-space:pre">			</span>
<span style="white-space:pre">		</span>}
<span style="white-space:pre">	</span>}
<span style="white-space:pre">	</span>
<span style="white-space:pre">	</span>/**
<span style="white-space:pre">	</span> * 判断权值
<span style="white-space:pre">	</span> * @param temp
<span style="white-space:pre">	</span> * @return
<span style="white-space:pre">	</span> */
<span style="white-space:pre">	</span>static int value(StringBuffer temp){
<span style="white-space:pre">		</span>
<span style="white-space:pre">		</span>int k=0;
<span style="white-space:pre">		</span>StringBuffer bz=new StringBuffer(temp);
<span style="white-space:pre">		</span>for (int i = 1; i <=bz.length(); i++) {
<span style="white-space:pre">			</span>StringBuffer nBuffer=new StringBuffer(bz.subSequence(i, bz.length()));
<span style="white-space:pre">			</span>StringBuffer mBuffer=new StringBuffer(bz.subSequence(0, i));
<span style="white-space:pre">			</span>nBuffer.append(mBuffer);
<span style="white-space:pre">			</span>//注意StringBuffer对象的equals为继承自Object的方法,只有在
<span style="white-space:pre">			</span>//非空引用,x,y,引用同一个对象时,才返回true
<span style="white-space:pre">			</span>
<span style="white-space:pre">			</span>//但是String的equals方法是自己重写的,比较两个字符串值是否相等
<span style="white-space:pre">			</span>if (nBuffer.toString().equals(temp.toString())) {
<span style="white-space:pre">				</span>k++;
<span style="white-space:pre">			</span>}
<span style="white-space:pre">		</span>}
<span style="white-space:pre">		</span>
<span style="white-space:pre">		</span>return k;
<span style="white-space:pre">		</span>
<span style="white-space:pre">	</span>}
<span style="white-space:pre">	</span>
<span style="white-space:pre">	</span>//获取全排列
<span style="white-space:pre">	</span>/**
<span style="white-space:pre">	</span> * 参考链接:http://blog.csdn.net/ly969434341/article/details/51271675
<span style="white-space:pre">	</span> * @param cur
<span style="white-space:pre">	</span> * @param start
<span style="white-space:pre">	</span> * @param length
<span style="white-space:pre">	</span> * @param rec
<span style="white-space:pre">	</span> */
<span style="white-space:pre">	</span>static void allRange(int[] cur,int start,int length,ArrayList<int[]>rec){
<span style="white-space:pre">		</span>if (start==length-1) {
<span style="white-space:pre">			</span>for (int i = 0; i < cur.length; i++) {
<span style="white-space:pre">				</span>System.out.print(cur[i]+" ");
<span style="white-space:pre">			</span>}
<span style="white-space:pre">			</span>System.out.println();
<span style="white-space:pre">			</span>
<span style="white-space:pre">			</span>//注意数组的引用性质,若不重新构建,只是将数组名赋予另一个变量时,只是对地址的
<span style="white-space:pre">			</span>//一个引用,数据只存有一份,当数据改变时,所有指向该数据的引用变量的值都改变了。
<span style="white-space:pre">			</span>int[] t=new int[length];
<span style="white-space:pre">			</span>for (int i = 0; i < t.length; i++) {
<span style="white-space:pre">				</span>t[i]=cur[i];
<span style="white-space:pre">			</span>}
//<span style="white-space:pre">			</span>t=cur;
<span style="white-space:pre">			</span>rec.add(t);
<span style="white-space:pre">		</span>}else {
<span style="white-space:pre">			</span>for (int i = start; i <=length-1; i++) {
<span style="white-space:pre">				</span>//从下标为start与后边分别交换
<span style="white-space:pre">				</span>swap(cur,start,i);
<span style="white-space:pre">				</span>allRange(cur, start+1, length, rec);
<span style="white-space:pre">				</span>swap(cur,start,i);
<span style="white-space:pre">			</span>}
<span style="white-space:pre">		</span>}
<span style="white-space:pre">	</span>}
<span style="white-space:pre">	</span>
<span style="white-space:pre">	</span>static void swap(int[] cur,int start,int i){
<span style="white-space:pre">		</span>int temp=cur[start];
<span style="white-space:pre">		</span>cur[start]=cur[i];
<span style="white-space:pre">		</span>cur[i]=temp;
<span style="white-space:pre">	</span>}
<span style="white-space:pre">	</span>	

对0 1  2,进行全排列:

第一次:位置0与0交换后仍为0 1 2
递归对1 2,进行全排列
第一次:位置1与1交换后仍为0 1 2
递归对2,进行全排列
由于2已到达末尾,故输出排列:0 1 2
对2 递归结束
将位置1与1交换复原后为0 1 2
第二次:位置1与2交换后0 2 1
递归对1,进行全排列
由于1已到达末尾,故输出排列:0 2 1
对1递归结束
将位置1与2交换复原后为0 1 2
对1 2 递归,以从开始到达尾末,递归结束
将位置0与0交换复原后仍为0 1 2
第二次:位置0与1交换后为1 0 2
递归对0 2,进行全排列
第一次:位置1与1交换后仍为1 0 2
递归对2,进行全排列
由于2已到达末尾,故输出排列:1 0 2
对2 递归结束
将位置1与1交换复原后为1 0 2
第二次:位置1与2交换后1 2 0
递归对0,进行全排列
由于0已到达末尾,故输出排列:1 2 0
对0递归结束
将位置1与2交换复原后为1 0 2
对0 2 递归,以从开始到达尾末,递归结束
将位置0与1交换复原后为0 1 2
第三次:位置0与2交换后为2 1 0
递归对1 0,进行全排列
第一次:位置1与1交换后仍为2 1 0
递归对0,进行全排列
由于0已到达末尾,故输出排列:2 1 0
对0递归结束
将位置1与2交换复原后为2 1 0
第二次:位置1与2交换后2 0 1
递归对1,进行全排列
由于1已到达末尾,故输出排列:2 0 1
对1递归结束
将位置1与2交换复原后为2 1 0
对1 0 递归,以从开始到达尾末,递归结束
将位置0与2交换复原后为0 1 2

对0 1 2递归全排列已从位置0依次交换过位置0 1 2,递归结束,输出了所有的全排列





去重的全排列

为了得到不一样的排列,可能我们最先想到的方法是当遇到和自己相同的就不交换了。如果我们输入的是abb,那么第一个字符与后面的交换后得到 babbba。然后abb中,第二个字符和第三个就不用交换了。但是对于bab,它的第二个字符和第三个是不同的,交换后得到bba,和之前的重复了。因此,这种方法不行。

因为abb能得到babbba,而bab又能得到bba,那我们能不能第一个bba不求呢? 我们有了这种思路,第一个字符a与第二个字符b交换得到bab,然后考虑第一个字符a与第三个字符b交换,此时由于第三个字符等于第二个字符,所以它们不再交换。再考虑bab,它的第二个与第三个字符交换可以得到bba。此时全排列生成完毕,即abbbabbba三个。

这样我们也得到了在全排列中去掉重复的规则:去重的全排列就是从第一个数字起每个数分别与它后面非重复出现的数字交换。用编程的话描述就是第i个数与第j个数交换时,要求[i,j)中没有与第j个数相等的数。下面给出完整代码:

#include<stdio.h>
#include<string>

//交换两个字符
void Swap(char *a ,char *b)
{
	char temp = *a;
	*a = *b;
	*b = temp;
}

//在 str 数组中,[start,end) 中是否有与 str[end] 元素相同的
bool IsSwap(char* str,int start,int end)
{
	for(;start<end;start++)
	{
		if(str[start] == str[end])
			return false;
	}
	return true;
}

//递归去重全排列,start 为全排列开始的下标, length 为str数组的长度
void AllRange2(char* str,int start,int length)
{
	if(start == length-1)
	{
		printf("%s\n",str);
	}
	else
	{
		for(int i=start;i<=length-1;i++)
		{
			if(IsSwap(str,start,i))
			{
				Swap(&str[start],&str[i]); 
				AllRange2(str,start+1,length);
				Swap(&str[start],&str[i]); 
			}
		}
	}
}

void Permutation(char* str)
{
	if(str == NULL)
		return;

	AllRange2(str,0,strlen(str));
}

void main()
{
	char str[] = "abb";
	Permutation(str);
}

全组合

如果不是求字符的所有排列,而是求字符的所有组合应该怎么办呢?还是输入三个字符 a、b、c,则它们的组合有a b c ab ac bc abc。当然我们还是可以借鉴全排列的思路,利用问题分解的思路,最终用递归解决。不过这里介绍一种比较巧妙的思路 —— 基于位图。

假设原有元素 n 个,则最终组合结果是  2n1  个。我们可以用位操作方法:假设元素原本有:a,b,c 三个,则 1 表示取该元素,0 表示不取。故取a则是001,取ab则是011。所以一共三位,每个位上有两个选择 0 和 1。而000没有意义,所以是 2n1 个结果。

这些结果的位图值都是 1,2…2^n-1。所以从值 1 到值  2n1  依次输出结果:

001,010,011,100,101,110,111 。对应输出组合结果为:a,b,ab,c,ac,bc,abc
因此可以循环 1~2^n-1,然后输出对应代表的组合即可。有代码如下:

#include<stdio.h>
#include<string.h>

void Combination(char *str)
{
	if(str == NULL)
		return ;
	int len = strlen(str);
	int n = 1<<len;
	for(int i=1;i<n;i++)    //从 1 循环到 2^len -1
	{
		for(int j=0;j<len;j++)
		{
			int temp = i;
			if(temp & (1<<j))   //对应位上为1,则输出对应的字符
			{
				printf("%c",*(str+j));
			}
		}
		printf("\n");
	}
}

void main()
{
	char str[] = "abc";
	Combination(str);
}

参考资料

-EOF-


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值