所谓全排列,就是将集合中元素的所有排列情况依次输出。比如{1、2、3}的全排列为:123、132、213、231、312、321,共6种,满足计算公式N!(N为集合中元素个数,不重复)。当元素不重复时,全排列采用递归思想较容易实现,它的递归公式推导步骤类似:1、要求得123的全排列,只需求得:1并上23的全排列(1 23, 1 32),2并上13的全排列(2 13, 2 31),3并上12的全排列(3 12 321)。2、对于23的全排列,只需求得2并上3的全排列,3并上2的全排列。步骤1中13、12的全排列也类似。3、对于3的全排列或者2的全排列,就是它们的本身。递归结束。递归实现不重复元素全排列算法的实现代码(C++)如下:
#include <iostream>
using namespace std;
//交换a和b
void Swap(int *a, int *b)
{
int t = *a;
*a = *b;
*b = t;
}
//全排列函数。list为待排元素列表,start为起始位置下标,end为最后一个有效元素的下一个下标。
void Permutation(int start, int end, int list[])
{
int i;
if (start >= end) //递归结束,打印当前这次全排列结果,返回。
{
for (i = 0; i < end; i++)
{
printf("%d ", list[i]);
}
printf("\n");
return;
}
//对于给定的list[start...end],要使区间中每一个元素都有放在第一位的机会,
//然后开始递归调用自身,得到list[start+1...end]的全排列。
for (i = start; i < end; i++)
{
Swap(&list[i], &list[start]); //交换元素,使得每一个元素都有放在第一位的机会。
Permutation(start+1, end, list); //递归调用
Swap(&list[i], &list[start]); //恢复原始的list,不影响下次递归调用。
}
}
int main()
{
int a[] = {1, 2, 3};
Permutation(0, 3, a);
return 0;
}
当待排元素列表含有重复项时,上述算法就需要改进,其中一种方法可以是维护一个存放不重复排列的集合,每次新生成1个排列,如果集合中不存在这个排列,则插入排列,否则,放弃。
要实现含重复元素的全排列算法,可以参考STL中next_premutation()函数的实现方法(在algorithm.h中声明)。该函数会将列表中元素按字典序(wiki [2])给出全排列中的下一个排列,它的实现算法为:
令当前排列为P(0)P(1)P(2)...P(n-1)P(n)。则求它下一个排列的过程为,
1、从后往前遍历,找到第一个P(i)>P(i-1)的元素,记录下标i。比如排列1、5、2、4、3中满足条件的元素为4,记下它的下标i = 3,因为P(i)是4,P(i-1)是2,满足P(i)>P(i-1)。如果找不到这样的i,则表示该序列已经是字典序中的最后一个序列,结束算法。
2、从后往前遍历,找到第一个P(j)>P(i-1)的数,记录下标k。还是上面这个例子,P(i-1)为2,从后往前第一个大于P(i-1)是P(4)=3,因此记录下j=4。
3、互换P(i-1)和P(j),得到新序列1、5、3、4、2。
4、将P[i...n]间的元素逆置,返回序列。上述例子中为逆置4和2,得到最终的序列1、5、3、2、4。
用比较通俗的例子解释一下上述步骤:
假设现在有一个序列4、6、5、3、2、1,要求得字典序的下一个序列。首先,从后往前找到第一个i,使得P(i)>P(i-1),明显这里i是1,P(i)=6,这个意思是,在6之后的元素,都是按值递减的,否则第一步求i的时候也不会找到第2个元素6才满足条件。现在知道,从i开始到最后,其实是字典序里的最大序列了(一直按值递减)。第二步,拿出i的前一个元素P(i-1)=4,将它与原序列从后往前第一个大于它的元素交换位置,这里这个与4交换的元素是5,这样序列就变成了5、6、4、3、2、1,至此,最高位升了一级(4->5),接着要把低位的从最大变成最小(就像199之后是200,最高为从1变成2后,要把低位从最大99变成最小00),这里的低位是最大序列6、4、3、2、1,变成最小序列只需逆置即可,变成1、2、3、4、6,原序列变为5、1、2、3、4、6,即为所求。
实现代码如下:
/**
*如果存在当前序列在字典序中的下一个排列,则返回true,
*否则返回false。
*/
bool next_premutation(int list[], int length)
{
int i, j;
//步骤1:得到i。
for (i = length - 1; i > 0; i--)
{
if (list[i] > list[i-1])
{
break; //记下下标i。
}
}
if (i <= 0)
{
//表示当前排列已经是字典序中的最后一个序列,没有下一个了。
return false;
}
//步骤2:得到j。
for (j = length - 1; j > 0 ; j--)
{
if (list[j] > list[i-1])
{
break; //记下下标j。
}
}
//步骤3:互换list[i-1]和list[j]。
int temp = list[i-1];
list[i-1] = list[j];
list[j] = temp;
//步骤4:逆置list[i...n]。
int start, end;
for (start = i, end = length-1; start < end; start++, end--)
{
int temp = list[start];
list[start] = list[end];
list[end] = temp;
}
return true;
}
采用这种方法要获得一个集合的全排列,可按下面方法调用(和stl函数next_permutation()的调用方法基本一致):
#include <iostream>
using namespace std;
int main()
{
int list[] = {1, 2, 3, 4, 5};
do
{
for (int i = 0; i < 5; i++)
{
printf("%d ", list[i]);
}
printf("\n");
}while (next_premutation(list, 5));
return 0;
}
两种算法的运行效率如下,这里比的是以上两种全排列实现算法之间的相对效率,不考虑硬件等因素,为了抹去I/O带来性能消耗,测试程序中已经打印部分代码(这部分开销很致命)注释,以下是在虚拟机环境下测得的对集合{1、2、3、4、5、6、7、8、9、10}的全排列计算时间:
me@ubuntu:~/premutation$ time ./Premutation
real 0m0.227s
user 0m0.212s
sys 0m0.012s
me@ubuntu:~/premutation$ time ./STL_Premutation
real 0m0.105s
user 0m0.096s
sys 0m0.004s
由此可见,相比递归方法(耗时0m0.227s),第二种方法更加高效(耗时real 0m0.105s),还能防重复。其实想想也能理解,第二种实现方法与STL的next_permutation()基本一致,理论上应该是接近最优解了吧。但是,采用第二种方法计算全排列也有一个前提,就是这个待排序列必须是字典序中的最小数,因此,需要在循环调用next_premutation()前将序列排序,否则只能得到当前序列之后的"全"排列。
--EOF--
Article printed from 衔山的博客: http://fengchangjian.com
URL to article: http://fengchangjian.com/?p=1063
URLs in this post:
[1] Permutation.cpp: http://fengchangjian.com/wp-content/uploads/2011/09/Permutation.cpp
[2] wiki: http://en.wikipedia.org/wiki/Lexicographical_order
[3] STL_Premutation.cpp: http://fengchangjian.com/wp-content/uploads/2011/09/STL_Premutation.cpp