全排列实现

来源:互联网 发布:js参数给jquery赋值 编辑:程序博客网 时间:2024/05/17 08:27

       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 32int 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 32int 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 个整型。

0 0
原创粉丝点击