非有序全排列生成算法
作者:goal00001111(高粱)
我曾经写过一篇《有序全排列生成算法》,介绍了五种生成有序全排列的方法,在该文的末尾,我计划再写一篇姊妹篇《非有序全排列生成算法》,由于各种原因,一直迟迟未动笔,前几天学习数据结构“栈”的时候,碰到一个有趣的问题“列车出栈序列”,其中有一种解法需要用到非有序全排列,所以决定先写好本文,再总结该问题。
生成非有序全排列的算法很多,有普通递归算法,循环移位法,邻位对换法,需要中介数的递增进位排列生成算法,递减进位排列生成算法和循环左移排列生成算法等。
我们先看最简单的普通递归算法,它的原理是:
如果用P表示n个元素的排列,而Pi表示不包含元素i的排列,(i)Pi表示在排列Pi前加上前缀i的排列,那么,n个元素的排列可递归定义为:
如果n=1,则排列P只有一个元素i
如果n>1,则排列P由排列(i)Pi构成(i=1、2、....、n-1)。
根据定义,容易看出如果已经生成了k-1个元素的排列,那么,k个元素的排列可以在每个k-1个元素的排列Pi前添加元素i而生成。
例如2个元素的排列是 1 2和2 1,对每个元素而言,p1是2 3和3 2,
在每个排列前加上1即生成1 2 3和1 3 2两个新排列,p2和p3则是1 3、3 1和1 2、2 1,按同样方法可生成新排列2 1 3、2 3 1和3 1 2、3 2 1。
主要代码如下:
/*
函数名称:Permutation
函数功能:普通递归算法:输出n个数的所有全排列
输入变量:int n:1,2,3,...,n共n个自然数
输出变量:无
*/
void Permutation(int n)
{
int *a = new int[n];//用来存储n个自然数
for (int i=0; i<n; i++) //存储全排列的元素值
a[i] = i + 1;
Recursion(a, n, n-1); //调用递归函数
delete []a;
}
/*
函数名称:Recursion
函数功能:递归输出n个数的所有全排列
输入变量:int a[]:存储了1,2,3,...,n共n个自然数的数组
int n:数组a[]的长度
int k:正在处理的k个元素所组成的排列
输出变量:无
*/
void Recursion(int a[], int n, int k)
{
if (k == 0) //排列只有一个元素a[k],直接输出
Print(a, n);
else
{
int temp;
for (int i=0; i<=k; i++) //穷举,依次让第k个元素与前面的元素交换
{
temp = a[i];
a[i] = a[k];
a[k] = temp;
Recursion(a, n, k-1); //递归生成k-1个元素的全排列
temp = a[i]; //再换回来
a[i] = a[k];
a[k] = temp;
}
}
}
void Print(int a[], int n)
{
for (int i=0; i<n; i++)
cout << a[i];
cout << endl;
}
循环移位法是一种很容易理解的非有序全排列算法,它的原理是:如果已经生成了k-1个元素的排列,则在每个排列后添加元素k使之成为k个元素的排列,然后将每个排列循环左移(右移),每移动一次就产生一个新的排列。
例如2个元素的排列是1 2和2 1。在1 2 后加上3成为新排列1 2 3,将它循环左移可再生成新排列2 3 1、3 1 2,
同样2 1 可生成新排列2 1 3、1 3 2和3 2 1。
代码也和简单,使用了递归穷举思想,我生成了一个循环左移的算法,有兴趣的读者可以自己生成一个循环右移的算法。
/*
函数名称:Permutation
函数功能:全排列循环移位法:输出n个数的所有全排列
输入变量:int n:1,2,3,...,n共n个自然数
输出变量:无
*/
void Permutation(int n)
{
unsigned int *a = new unsigned int[n];//用来存储n个自然数
for (int i=0; i<n; i++) //存储全排列的元素值,并计算全排列的数量
{
a[i] = i + 1;
}
Recursion(a, n, 1);
delete []a;
}
/*
函数名称:Recursion
函数功能:循环左移递归输出n个数的所有全排列
输入变量:int a[]:存储了1,2,3,...,n共n个自然数的数组
int n:数组a[]的长度
int k:正在处理的k个元素所组成的排列
输出变量:无
*/
void Recursion(unsigned int a[], int n, int k)
{
if (k > n)
Print(a, n);
else
{
int temp;
for (int i=0; i<k; i++)//循环左移
{
temp = a[0];
for (int j=1; j<k; j++)
a[j-1] = a[j];
a[k-1] = temp;
Recursion(a, n, k+1);
}
}
}
一个能够快速生成全排列的算法叫做邻位对换法,它之所以较快,是因为邻位对换法中下一个排列总是上一个排列某相邻两位对换得到的,只需一步,就可以得到一个新的全排列,而且绝不重复,但是由于每将n从一端移动到另一端后,就需要遍历排列2次,来寻找最大的可移数m,所以速度得到了限制。它的原理是:
[n]的全排列可由[n-1]的全排列生成:
给定[n-1]的一个排列п,将n 由最右端依次插入排列п ,即得到n个[n]的排列:
p1 p2…pn-1n
p1 p2…npn-1
np1 p2…pn-1
对上述过程,一般地,对i,将前一步所得的每一排列重复i次,然后将i由第一排的最后往前移,至最前列,正好走了i次,下一个接着将i放在下一排列的最前面,然后依次往后移,一直下去即得i元排列。
考虑{1,2…n}的一个排列,其上每一个整数都给了一个方向,如果它的箭头所指的方向的邻点小于它本身,我们称整数k是可移的。
显然1永远不可移,n除了以下两种情形外,它都是可移的:
(1) n是第一个数,且其方向指向左侧
(2) n是最后一个数,且其方向指向右侧
于是,我们可按如下算法产生所有排列:
1,开始时:存在排列123…n,除1外,所有元素均可移,即方向都指向左侧。
2,当最大元素n可移动时,将其从一端依次移动到另一端,即可得到n-1个全排列;当n移动到某一端后,不能再移动,此时寻找最大的可移数m,将m与其箭头所指的邻数互换位置,这样就又得到一个新的全排列;将所得新排列中所有比m大的数p的方向调整,即改为相反方向,这样使得n又成了可移数。
3,重复第2步直到所有的元素都不能移动为止。
以4个元素的排列为例,首先生成全排列1 2 3 4;
找到最大的可移数4,将4与其箭头所指的邻数互换位置,可以生成3个新排列:
1 2 4 3 1 4 2 3 4 1 2 3
因为没有比4更大的数p,所以无需调整p的方向。最大数4到了最左边后,由于其方向指向左侧,所以4不能再移动。
接下来寻找最大的可移数3,将3与其箭头所指的邻数2互换位置,可以得到新排列:4 1 3 2;
然后将所得排列中比3大的数4的方向调整,使得4可以移动,重新成为最大的可移数,将4与其箭头所指的邻数互换位置,可以生成3个新排列:
1 4 3 2 1 3 4 2 1 3 2 4
此时最大数4到了最右边,又因为其方向指向右侧,所以4不能再移动;于是我们寻找最大的可移数3,将3与其箭头所指的邻数1互换位置,可以得到新排列:3 1 2 4;
然后将所得排列中比3大的数4的方向调整,使得4可以移动,重新成为最大的可移数,将4与其箭头所指的邻数互换位置,可以生成3个新排列:
3 1 4 2 3 4 1 2 4 3 1 2
如此循环,直到所有的数都不能移动,即可求出全部排列。最后得到的一个全排列为:2 1 3 4,此时2指向左侧,1,3,4均指向右侧。
根据上述算法分析,使用一个辅助数组来存储各个元素的指向,我们可以得到代码如下:
/*
函数名称:Permutation
函数功能:排列邻位对换法:输出n个数的所有全排列
输入变量:int n:1,2,3,...,n共n个自然数
输出变量:无
*/