全排列实现

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值