(总结)三种全排列算法

来源:互联网 发布:树莓派3 gpio 编程 编辑:程序博客网 时间:2024/06/05 17:04

 1、递归算法

    算法书上有详细的解释,复制如下:

    设R={r1,r2,...,rn}是要进行排列的n个元素,Ri=R-{ri}.集X中元素的全排列记为Perm(X),(ri)Perm(X)表示在全排列Perm(X)的每一个排列前加上前缀ri得到的排列.R的全排列可归纳定义如下:
  当n=1时,Perm(R)={r},r是集合R中唯一的元素.
  当n>1时,Perm(R)由(r1)Perm(R1),(r2)Perm(R2),....(rn)Perm(Rn)构成

   代码如下:

   

2、stl模板中next_permutation

对范型算法next_permutation的源码分析 

// TEMPLATE FUNCTION next_permutation
    template<class _BI> inline
    bool next_permutation(_BI _F, _BI _L)
    {

        _BI _I = _L;
       if (_F == _L || _F == --_I)  // (1)
            return (false);
       for (; ; ){

           _BI _Ip = _I;  // (2)
           if (*--_I < *_Ip)   // (3){

              _BI _J = _L;
               for (; !(*_I < *--_J); ) ; // (4)
               iter_swap(_I, _J);  // (5)
               reverse(_Ip, _L);  // (6)
                return (true);

        }
        if (_I == _F){

            reverse(_F, _L);  // (7)
            return (false);

     }

  }

}

next_permutation()将[first,last)标记的排列重排为下一个排列,如果不存在下一个排列,则返回false。
源码分析(假设有容器{3,5,4,2}):
(1)、由于范型算法一般是应用在容器上,而容器是用其首元素和最后元素的下一个位置来表示其范围的,所以next_permutation()处理的 [first,last)为半开半闭区间,对于算法内部,则很明显要先对_I进行递减操作,使其指向容器的最后一个元素,如果和首元素地址相同,则为单元素容器,不存在下一个排列。
(2)、把_I的iter赋给_lp,注意对于第一次循环_I是指向最后元素了(因为(1)中有操作--I)。_Ip和_I指向容器元素5时,(3)的条件满足。
(3)、因其处在无限循环for(;;)中,故在容器中从后往前找第一个非逆序(后面的元素不小于其紧挨的之前的元素*--I<*_lp)(3,5),此时_I指向3,_lp指向5。
(4)、对_I到最后一个元素之间的元素查询是否有大于_I所指向的元素(3),有!此时_J指向元素5大于3。如果没有,那么该for循环最后_I,_J指向同一元素3,同样结束该for循环。
(5)、交换_I,_J所指向的元素。得到容器{4532}
(6)、对于_lp和_L之间的元素重新按反序排列,原来为逆序532,现在重排为235,于是得到容易{4235}  ,即是容器{3542}的下一个排列。
(7)、对于已经排成完全逆序的容器,如{5432},则先将其按反序重排为完全顺序{2345},然后返回false,因为其不存在下一个排列。

注意:在求容器的下一个排列时,总是要保证容器靠前的元素最小的不同排列,且如果得到完全逆序容器,则返回false,所以如果要求一个容器的全排列,应该先对该容器进行顺序排序,最后一点经过next_permutation()操作最后返回false后的容器为顺序的,应为该算法对完全逆序容器是先反序排列了之后再返回false的。

    此算法进行的全排列的话,需要首先对排列的数组进行排序。假设其为升序排列。

    首先,从右至左,找出第一个按升序排列的数对,比如上列中{3,5,4,2}中的(3,5),这说明从五之后的都是逆序,既降序排列。

   然后从降序排列的数中(5,4,2),找到第一个比3大的数,并与3交换,因此可得到{4,5,3,2};

  交换后可以保证从5开始之后的数都是降序的(5,3,2),对他进行反转,既(2,3,5),总的为(4,2,3,5),此即为stl算法中的下一个排列。

  当对(4,2,3,5)之后的排列,其实是会对子串(2,3,5)进行全排列的输出。因此感觉其是递归回溯的思想。

3、奇妙的非递归算法取全排列

    摘抄如下:

1. 算法思路:
           试想N个自然数A1,A2,...,AN的全排列是N!个,那么,对于每种排列,我将其打印出来,遍历一遍至少是O(N!)的复杂度,那么能不能在就在这个复杂度内用非递归解决这个问题呢?我的想法就是从这点开始成形.同时借助了建模的思想分析的.
           无论如何,我首先选取一个自然数 A1 出来,放到存放地址的中间, 那么在A1的周围存在两个位置,一个是A1的左边,一个是A1的右边,那么,我抽出另一自然数出来(假设是AK),便有两种存放的可能性:
(AK,A1) , (A1,AK).
           如果我将1周围的两个位置用0和1来表示,如果放在左边,那么左边用1表示,反之用0表示.反正只能放一个地方,那么这两个位置合起来不是10,就是01,化成十进制便是2或者1.
           同理,如果第第二个数A2放在A1的左边,也就是第一轮获取了2,排列就是(A2,A1),当第三个数(假设是A3)到来时,那么它就存在三个位置可放了,分别是A2的左边,A2和A1之间,A1的右边,同样用0和1标志每个位置,如果A3放在A2左边,那么构成100(十进制4),如果中间则构成010(十进制2),A1的右边001(十进制1),那么可以用第二个值来表征第三个数摆放的位置.
           如此,细心的玩家已经大概想明白了我要干嘛了,对,如此以往,可以将全部N个自然数用N-1个数值来替代(这些数字是有规律的,2的次方),接下来,我想到的便是,一定要找到这些数字和它在全排列中一一对应的关系,那么问题便解决了.
           我们拿3个自然数的例子来分析,3!=6,对于3个自然数的全排列我将其编号1-6组,每组按上面的规则应该可以唯一对应一个序列,如将第一组对应于(1,1),  是按上面规则来的.  第二组对应于(1,2), 第三组对应于(2,1), 第四组对应于(2,2), 第五组对应于(4,1), 第六组对应于(4,2). 于是, 我只要分析出一个计算规则便可以只要遍历这N!个分组,便可以求出每个分组对应的排列了.
           很显然, 由P(M,M)的计算规则,P(M,M)=M*(M-1)*...*1可以看出,如果我在第2个到来时,将m = P(M,M)%2 同时,P=P/2,那么,可以很好的处理第二个到来时所做的寻址选择:如果m是0,那么就放右边,如果m是1,就放左边,而对应于第三个到来时,同样如此,那么这个存放规则便生成了.于是遍历N!次,按辗转法则,便可以直接生成每个组对应的全排列了.

      个人感觉作者的思路说的有些太复杂了,这个算法的目的就是说对于n个数,一共有n!种排列,假设从0-(n!-1)个数字中每一个数字可以对应一种排列,而如何建立其这n!个数和某种排列的对应关系,是这个算法的关键。

     我们总是假设第一个数已经放好了,对于第二个数,有两种选择,对于第三个数有三种选择。。。。第k个数有k种选择。对于一个在0-(n!-1)之间的数字m,和第二个数的选择,可以用公式m%2,得到的结果为0或者1,既可以代表第二个数的两种选择,之后必须用m = m/2,因为第二个数的已经做了选择,剩余的m,是之后数字的选择的结果。对于第k个数,公式为 m%k, 和 m=m/k。

 代码于分析(补)。。。。

原创粉丝点击