生成排列的非递归实现算法

1.使用标记数组

例如,我们要从集合{1,2,……,n}中选取r个元素排列输出。我们可以检查每一个位数不大于rn进制数,如果它们是符合条件的排列(各个数位互不相同),则将其输出。

为了实现这样的检查,我们需要引入一个标记数组,用来标识哪些元素已经被选取过。

#include <stdio.h>
#define N 99    //集合中最大的元素个数

int main()
{
	int a[N + 1];//最终输出的排列结果为:a[1],……,a[r]
	int d[N + 1];//如果元素i已经被选取,那么d[i]=1,否则d[i]=0
	int i, j;
	int n;//集合中的元素个数
	int r;//要选取进行排列的元素个数
	double count;//当前已经找到多少个符合条件的排列
	while (1)
	{
		//读取用户输入的n和r
		scanf("%d%d", &n, &r);
		if ((n <= 0) || (n > N)) { break; }
		//初始排列为1,1,……,1(显然有重复选取,不符合条件)
		for (i = 1; i <= r; i++) { a[i] = 1; }
		count = 0;
		//每次循环生成一个备选的排序方案
		while (1)
		{
			//标记数组清零
			for (j = 1; j <= n; j++) { d[j] = 0; }
			d[0] = 1; a[0] = 0;//边界哨兵
			//检查该排列方案是否存在重复选取的情况
			i = r;
			d[a[i]]++;
			while (d[a[i]] == 1) 
			{
				i--;
				d[a[i]]++;
				//当i等于0时,边界哨兵d[0]==2,跳出循环
			}
			//i等于0说明该排列方案不存在重复选取的情况,可以输出
			if (i == 0) 
			{
				count++; printf("%6.0f: ", count);
				for (i = 1; i <= r; i++) { printf("%2d ", a[i]); }
				printf("\n");
			}
			//生成下一个备选的排序方案
			i = r;
			a[i]++;
			//如果有元素超出范围,需要重新调整(进位)
			while (a[i] > n) 
			{
				a[i] = 1;
				i--;
				a[i]++;
			}
			//i等于0说明已经遍历完了所有的备选方案,可以跳出循环
			if (i == 0) { break; }
		}
		//输出最终结果
		printf("n=%d r=%d count=%16.0f\n", n, r, count);
	}
}

存在的问题:
一共考虑了n的r次方个备选排列方案,但实际上只有n!/(n-r)!个是符合条件的,造成了大量的性能浪费。

不光如此,这里为了简化编程,也没有很好地发挥标记数组的记忆功能,而是每次检测前都对其进行清零操作,然后需要重新遍历才能得出改排列方案是否合法的结论。

2.改进地使用标记数组

为了更好地发挥标记数组的功能,进一步提高检查合法性的效率,我们可以将代码改进为下面这样子:

#include <stdio.h>
#define N 99
int main() 
{
	int a[N + 1];//最终输出的排列结果为:a[1],……,a[r]
	int d[N + 2];//如果元素i已经被选取,那么d[i]=1,否则d[i]=0
	int i, j;
	int k;//标识当前操作元素为a[k]
	int n;//集合中的元素个数
	int r;//要选取进行排列的元素个数
	double count;//当前已经找到多少个符合条件的排列
	while (1)
	{
		//读取用户输入的n和r
		scanf("%d%d", &n, &r);
		if ((n <= 0) || (n > N)) { break; }
		//初始排列为1(后面的元素未确定),当前操作元素为a[k]=a[1]
		a[1] = 1;	k = 1;
		for (j = 1; j <= n; j++) { d[j] = 0; }
		d[n + 1] = 1;//边界哨兵
		count = 0;
		//生成不完整的排列方案
		while (1) 
		{
			if (d[a[k]] == 1)//元素a[k]已经被选取过,或者触发边界哨兵的条件d[n + 1] == 1
			{
				if (a[k] == n + 1) 
				{
					//如果触发边界哨兵的条件d[n + 1] == 1,则返回上一位操作元素
					k--;
					if (k == 0) 
					{
						break;//已经没有再上一位的操作元素了,可以退出程序
					}
					//更新a[k]为未选取过
					d[a[k]] = 0;
				}
				//a[k]取下一个值
				a[k]++;
			}
			else//元素a[k]未被选取过
			{
				//更新a[k]为已经被选取过
				d[a[k]] = 1;
				//操作下一个元素
				k++; a[k] = 1;
				//已经足够r个元素(它们都互不相同),可以输出
				if (k > r) 
				{
					count++; printf("%6.0f: ", count);
					for (i = 1; i <= r; i++) { printf("%2d ", a[i]); }
					printf("\n");
					//k的值实际上变成r,因为a[k]要取下一个值,所以要更新a[k](旧值)为未选取过
					k--; d[a[k]] = 0;
					//a[k]要取下一个值
					a[k]++;
				}
			}
		}
		//输出最终结果
		printf("n=%d r=%d count=%16.0f\n", n, r, count);
	}
}

3.按字典序排列算法

注意: 该算法只适用于全排列,即不能只选取部分元素进行排列(上面的r只能等于n)。

其基本思想是:

1.对初始队列进行排序,找到所有排列中最小的一个排列Pmin

2.找到刚刚好比Pmin大比其它都小的排列P(min+1)

3.循环执行第二步,直到找到一个最大的排列,算法结束。

其核心就在于,如何找到那个比给定排列大的最小排列(字典序),算法如下:

①从右向左寻找第一个由增加转为减少的元素(这里是6
操作1.png

②从6开始往右找到比它大的最小元素(这里是7),并将它们交换位置
操作2.png

③将交换后的右边的所有序列倒转(这里是9653
操作3.png

#include <iostream>  
#include <cstring>  
using namespace std;  
  
//交换数组a中下标为i和j的两个元素的值  
void swap(int *a,int i,int j)  
{  
    a[i]^=a[j];  
    a[j]^=a[i];  
    a[i]^=a[j];  
}  
  
//将数组a中的下标i到下标j之间的所有元素逆序倒置  
void reverse(int a[],int i,int j)  
{  
    for(; i<j; ++i,--j) {  
        swap(a,i,j);  
    }  
}  
  
void print(int a[],int length)  
{  
    for(int i=0; i<length; ++i)  
        cout<<a[i]<<" ";  
    cout<<endl;  
}  
  
//求取全排列,打印结果  
void combination(int a[],int length)  
{  
    if(length<2) return;  
  
    bool end=false;  
    while(true) {  
        print(a,length);  
  
        int i,j;  
        //找到不符合趋势的元素的下标i  
        for(i=length-2; i>=0; --i) {  
            if(a[i]<a[i+1]) break;  
            else if(i==0) return;  
        }  
  
        for(j=length-1; j>i; --j) {  
            if(a[j]>a[i]) break;  
        }  
  
        swap(a,i,j);  
        reverse(a,i+1,length-1);  
    }  
}  
int main(int argc, char **argv)  
{  
    int a[4] = {1, 2, 3, 4};  
    combination(a, sizeof(a) / sizeof(int));  
    return 0;  
}  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值