您的位置:首页 > 编程语言 > C语言/C++

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

2017-06-29 00:23 316 查看

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

1.使用标记数组

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

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

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


存在的问题:

一共考虑了nr个备选排列方案,但实际上只有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)



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



③将交换后的右边的所有序列倒转(这里是9653)



#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;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息