n 个元素的全排列有 n! 个,相信学过概率的都知道。n = 2, 3, 4 的时候,就有 2, 6, 24个,还可以列出来。再往上,想全部直接列出来就有点难度了。如果交给计算机去做,计算机又该按照何种规则去列出这些组合呢?
我的第一想法是先从 n 个元素中拿出一个,放到目标数组的第一位生成 n 个数组,然后再从剩下的 n - 1 个元素中取出一个放到这 n 个数组的第二位 生成 n * (n - 1) 个数组,一层层往下,最终生成 n! 种排列。因为这种方法生成排列的方式都死按顺序在集合里面取的,所以这 n! 种排列肯定没有重复。下面是用C递归实现的把 n 个字符全排序,为了不破坏原数据,先把一个数组排序,然后根据下标输出字符。
#include <stdio.h>
#define MAX_NUM 32
int giMemory[MAX_NUM];
//pMyData 要全排列的组合
//iMax 组合的大小
//iFlag 用位标识组合中的元素是否被用过
//piNum 用来存放排列顺序的数组
//iNum 组合中的已经被取出的元素个数
//pFun 生存一个排列的时候,用来处理的函数
int perm(void *pMyData, int iMax, int iFlag, int *piNum, int iNum, void pFun(void*, int, int*))
{
int i = 0;
if(iMax == iNum)
{
pFun(pMyData, iMax, piNum);
}
else
{
for(i = 0; i < iMax; i++)
{
if(!((1 << i) & iFlag))
{
piNum[iNum] = i + 1;
perm(pMyData, iMax, (1 << i) | iFlag, piNum, iNum + 1, pFun);
}
}
}
return 0;
}
int all_perm(void *pMyData, int iMax, void pFun(void*, int, int*))
{
perm(pMyData, iMax, 0, giMemory, 0, pFun);
return 0;
}
void process(void *pMyData, int iNum, int *piNum)
{
int i = 0;
char *pcData = (char *)pMyData;
for(i = 0; i < iNum; i++)
{
printf("%c ", pcData[piNum[i] - 1]);
}
printf("\n");
}
int main()
{
int iMax = 3, i = 0;
char gcMyData[MAX_NUM];
scanf("%d", &iMax);
iMax = ((unsigned int)iMax) > MAX_NUM ? MAX_NUM : iMax;
for(i = 0; i < iMax; i++)
gcMyData[i] = 'A' + i;
all_perm(gcMyData, iMax, process);
return 0;
}
因为 iFlag 是32位整数,所以限定 n 最大是32,但其实用C++的 bit set 或者用C的数组去标识也是可以的,只是我的破电脑对那种计算量是有心无力的,放大限制也没用。从上面可以看到,这种做法相当的简单,几句代码就搞定。但是缺点也还是相当的明显的,每次去一个数的时候还去一个个地扫,虽然本身 n 不大,但是扫的次数太多了,时间还是耗费了不少的。
然后就是我想到的第二种做法,通过交换把一个排列变成另外一个排列。通过把第 2 到 n 个元素交换到组合的第一位生成 n - 1 个排列加上第一个元素在第一位那个就有 n 种排列,在分别把这些排列的 n - 1 个元素交换到这些数组的第二位生成 n * (n - 1) 个,依此类推,最终生成 n! 个排列。到这里,再想一想,貌似和第一种做法差不多,只不过是在同一个数组内进行而已...... 下面照样还是一样功能的C程序:
#include <stdio.h>
#define MAX_NUM 32
int giMemory[MAX_NUM];
#define SWAP_INT(iA, iB) \
do \
{ \
int iTmp = iA; \
iA = iB; \
iB = iTmp; \
}while(0)
int perm(void *pMyData, int iMax, int *piNum, int iNum, void pFun(void*, int, int*))
{
int i = 0;
if(1 == iNum)
{
pFun(pMyData, iMax, piNum - (iMax - iNum));
}
else
{
for(i = 0; i < iNum; i++)
{
if(i)
{
SWAP_INT(piNum[0], piNum[i]);
perm(pMyData, iMax, piNum + 1, iNum - 1, pFun);
SWAP_INT(piNum[0], piNum[i]);
}
else
{
perm(pMyData, iMax, piNum + 1, iNum - 1, pFun);
}
}
}
return 0;
}
int all_perm(void *pMyData, int iMax, void pFun(void*, int, int*))
{
int i = 0;
for(i = 0; i < iMax; i++)
giMemory[i] = i + 1;
perm(pMyData, iMax, giMemory, iMax, pFun);
return 0;
}
void process(void *pMyData, int iNum, int *piNum)
{
int i = 0;
char *pcData = (char *)pMyData;
for(i = 0; i < iNum; i++)
{
printf("%c ", pcData[piNum[i] - 1]);
}
printf("\n");
}
int main()
{
int iMax = 3, i = 0;
char gcMyData[MAX_NUM];
scanf("%d", &iMax);
iMax = iMax > MAX_NUM ? MAX_NUM : iMax;
for(i = 0; i < iMax; i++)
gcMyData[i] = 'A' + i;
all_perm(gcMyData, iMax, process);
return 0;
}
这些用递归实现的全排列都非常的短小,也不难理解,但是在普通PC上效率是硬伤,n = 10 的时候已经要好几秒的计算时间。有没有更快的实现方法呢?
肯定是有的啦,递归慢是因为有太多的出入栈,再怎么改,效率也不会有多少改观。这时候肯定是要想方设法把递归转为迭代的了。一开始我是想把上面的算法转成迭代的,后来深入一想,这得多少存储空间啊,还是算了。就去网上搜一下全排列的迭代实现,发现有些其实还是递归....最后在百度百科里面看到了Heap,目前全排列最快的算法。但这个Heap和heap是没有任何关系的,不是说堆就比栈快,这个Heap其实是个人名。
他提出的一个算法是通过交换两个元素的位置得到下一个组合,通过一连串的交换生成 n! 个组合。貌似听起来好像和我第二个做法差不多,其实不是的,他老人家一连串的交换是串行的,元素位置交换就不换回来了,不像我的那个,交换了,还要交换回来。这样子一来一个组合的生成就只依赖于上一个组合,就不用额外的存储空间了,也更加容易用迭代实现。
那怎样交换才可以不重复出现同一个组合,而且还容易用迭代实现呢?他老人家是这样想的,如果要做 n 个元素的全排列,我可以先排好前面 n - 1 个数,得到 (n - 1)! 个排列,然后再把第 n 个数和前面 n - 1 个数的其中一个交换,又得到 (n - 1)! 个,依次类推就可以获得了全部的排列。因而只要把前面 n - 1 个数不重复地置换到最后一个位置就可以实现全排列。下面是置换的规则:
如上图所示,n = 4 的全排列包含了 4次 n = 3 时的全排列,n = 3 的全排列包含了 3 次 n = 2 的全排列。而且在排好前面 n - 1 个数的时候,当 n 是奇数的时候,总是和组合的第一个元素交换。当 n 是偶数的时候则相对复杂点,分别和 1,2,3 ....... (n - 1)个数交换。如果真要证明的话,可以用数学的归纳法去证,这里就不去证明了。用C实现上面程序一样功能的代码如下所示:
#include <stdio.h>
#define MAX 100
#define SWAP_INT(iA, iB) \
do \
{ \
int iTmp = iA; \
iA = iB; \
iB = iTmp; \
}while(0)
void process(char *pcData, int iNum, int *piNum)
{
int i = 0;
for(i = 0; i < iNum; i++)
{
printf("%c ", pcData[piNum[i]]);
}
printf("\n");
}
void perm(char *pcList, int iNum)
{
int i = 2, iCount[MAX], iSwapIdx, giNum[MAX];
for(i = 0; i < iNum; i++)
{
iCount[i] = 0;
giNum[i] = i;
}
process(pcList, iNum, giNum);
for(i = 1; i < iNum; i++)
{
if(iCount[i] < i)
{
if(i & 1)
iSwapIdx = iCount[i];
else
iSwapIdx = 0;
SWAP_INT(giNum[i], giNum[iSwapIdx]);
iCount[i] ++;
i = 0;
process(pcList, iNum, giNum);
}
else
{
iCount[i] = 0;
}
}
}
int main()
{
char gcData[MAX];
int iMax = 0, i = 0;
scanf("%d", &iMax);
iMax = iMax > MAX ? MAX : iMax;
for(i = 0; i < iMax; i++)
gcData[i] = 'A' + i;
perm(gcData, iMax);
return 0;
}
上面的代码看起来很短,但是理解起来还有有点难度的。首先要理解的是 iCount[MAX] 的作用,这里是用来做计数器的。按照上面的规则来做,第 k 个位置要和前面 k - 1 个位置交换 k - 1 次, k 个数才算完成全排列对吧。C语言下标是从0开始的,所以 iCount[i] 表示的就是第 i + 1 个位置和前面 i 个位置交换的次数。当前面 k 个数都排完了,把 k + 1 位置的换到前面 k 个位置后,又开始一轮的 k 个数排列,所以 iCount[k] 是要清零的。第二个是 i 在没次交换后都赋值为0的原因,其实应该是赋值为 1 的,因为根据归纳法的推断,前面 k 个数的第一个排列肯定是第1和第2个数交换,这里赋值为0,for循环后是1,也就是现实中的2。第三个要理解地方是条件判断里面的东西,一还是那个原因C语言下标从0,开始,所以这里的偶数就是上面规则的奇数。二是因为计数器刚好是逐步从1递增到 n - 1 的,所以做偶数(现实规则)时的那个位置的下标。
迭代实现的Heap虽然难理解,但是性能杠杠的,也不占空间,如果不怕破坏原来的数据的话,辅助空间也就 n 个整型。