全排列算法

https://www.cnblogs.com/nowornever-L/p/6008954.html

对于给定的集合A{a1,a2,...,an},其中的n个元素互不相同,如何输出这n个元素的所有排列(全排列)。

递归算法

这里以A{a,b,c}为例,来说明全排列的生成方法,对于这个集合,其包含3个元素,所有的排列情况有3!=6种,对于每一种排列,其第一个元素有3种选择a,b,c,对于第一个元素为a的排列,其第二个元素有2种选择b,c;第一个元素为b的排列,第二个元素也有2种选择a,c,……,依次类推,我们可以将集合的全排列与一棵多叉树对应。如下图所示

在此树中,每一个从树根到叶子节点的路径,就对应了集合A的一个排列。通过递归算法,可以避免多叉树的构建过程,直接生成集合A的全排列,代码如下。

 

 

 1 template <typename T>

 2 inline void swap(T* array, unsigned int i, unsigned int j)

 3 {

 4     T t = array[i];

 5     array[i] = array[j];

 6     array[j] = t;

 7 }

 8 

 9 /*

10  * 递归输出序列的全排列

11  */

12 void FullArray(char* array, size_t array_size, unsigned int index)

13 {

14     if(index >= array_size)

15     {

16         for(unsigned int i = 0; i < array_size; ++i)

17         {

18             cout << array[i] << ' ';

19         }

20 

21         cout << '\n';

22 

23         return;

24     }

25 

26     for(unsigned int i = index; i < array_size; ++i)

27     {

28         swap(array, i, index);

29 

30         FullArray1(array, array_size, index + 1);

31 

32         swap(array, i, index);

33     }

34 }

 

 

该算法使用原始的集合数组array作为参数代码的28~32行,将i位置的元素,与index位置的元素交换的目的是使得array[index + 1]到array[n]的所有元素,对应当前节点的后继结点,递归调用全排列生成函数。调用结束之后还需要回溯将交换位置的元素还原,以供其他下降路径使用。

字典序

全排列生成算法的一个重要思路,就是将集合A中的元素的排列,与某种顺序建立一一映射的关系,按照这种顺序,将集合的所有排列全部输出。这种顺序需要保证,既可以输出全部的排列,又不能重复输出某种排列,或者循环输出一部分排列。字典序就是用此种思想输出全排列的一种方式。这里以A{1,2,3,4}来说明用字典序输出全排列的方法。

首先,对于集合A的某种排列所形成的序列,字典序是比较序列大小的一种方式。以A{1,2,3,4}为例,其所形成的排列1234<1243,比较的方法是从前到后依次比较两个序列的对应元素,如果当前位置对应元素相同,则继续比较下一个位置,直到第一个元素不同的位置为止,元素值大的元素在字典序中就大于元素值小的元素。上面的a1[1...4]=1234和a2[1...4]=1243,对于i=1,i=2,两序列的对应元素相等,但是当i=2时,有a1[2]=3<a2[2]=4,所以1234<1243。

使用字典序输出全排列的思路是,首先输出字典序最小的排列,然后输出字典序次小的排列,……,最后输出字典序最大的排列。这里就涉及到一个问题,对于一个已知排列,如何求出其字典序中的下一个排列。这里给出算法。

  • 对于排列a[1...n],找到所有满足a[k]<a[k+1](0<k<n-1)的k的最大值,如果这样的k不存在,则说明当前排列已经是a的所有排列中字典序最大者,所有排列输出完毕。
  • 在a[k+1...n]中,寻找满足这样条件的元素l,使得在所有a[l]>a[k]的元素中,a[l]取得最小值。也就是说a[l]>a[k],但是小于所有其他大于a[k]的元素。
  • 交换a[l]与a[k].
  • 对于a[k+1...n],反转该区间内元素的顺序。也就是说a[k+1]与a[n]交换,a[k+2]与a[n-1]交换,……,这样就得到了a[1...n]在字典序中的下一个排列。

这里我们以排列a[1...8]=13876542为例,来解释一下上述算法。首先我们发现,1(38)76542,括号位置是第一处满足a[k]<a[k+1]的位置,此时k=2。所以我们在a[3...8]的区间内寻找比a[2]=3大的最小元素,找到a[7]=4满足条件,交换a[2]和a[7]得到新排列14876532,对于此排列的3~8区间,反转该区间的元素,将a[3]-a[8],a[4]-a[7],a[5]-a[6]分别交换,就得到了13876542字典序的下一个元素14235678。下面是该算法的实现代码

 

 

/*

 * 将数组中的元素翻转

 */

inline void Reverse(unsigned int* array, size_t array_size)

{

    for(unsigned i = 0; 2 * i < array_size - 1; ++i)

    {

        unsigned int t = array[i];

        array[i] = array[array_size - 1 - i];

        array[array_size - 1 - i] = t;

    }

}

 

inline int LexiNext(unsigned int* lexinum, size_t array_size)

{

    unsigned int i, j, k, t;

 

    i = array_size - 2;

 

    while(i != UINT_MAX && lexinum[i] > lexinum[i + 1])

    {

        --i;

    }

 

    //达到字典序最大值

    if(i == UINT_MAX)

    {

        return 1;

    }

 

    for(j = array_size - 1, k = UINT_MAX; j > i; --j)

    {

        if(lexinum[j] > lexinum[i])

        {

            if(k == UINT_MAX)

            {

                k = j;

            }

            else

            {

                if(lexinum[j] < lexinum[k])

                {

                    k = j;

                }

            }

        }

    }

 

    t = lexinum[i];

    lexinum[i] = lexinum[k];

    lexinum[k] = t;

 

    Reverse(lexinum + i + 1, array_size - i - 1);

    return 0;

}

 

/*

 * 根据字典序输出排列

 */

inline void ArrayPrint(const char* array, size_t array_size, const unsigned int* lexinum)

{

    for(unsigned int i = 0; i < array_size; ++i)

    {

        cout << array[lexinum[i]] << ' ';

    }

 

    cout << '\n';

}

 

/*

 * 基于逆序数的全排列输出

 */

void FullArray(char* array, size_t array_size)

{

    unsigned int lexinumber[array_size];

 

    for(unsigned int i = 0; i < array_size; ++i)

    {

        lexinumber[i] = i;

    }

 

    ArrayPrint(array, array_size, lexinumber);

 

    while(!LexiNext(lexinumber, array_size))

    {

        ArrayPrint(array, array_size, lexinumber);

    }

}

 

 

使用字典序输出集合的全排列需要注意,因为字典序涉及两个排列之间的比较,对于元素集合不方便比较的情况,可以将它们在数组中的索引作为元素,按照字典序生成索引的全排列,然后按照索引输出对应集合元素的排列,示例代码使用的就是此方法。对于集合A{a,b,c,d},可以对其索引1234进行全排列生成。这么做还有一个好处,就是对于字典序全排列生成算法,需要从字典序最小的排列开始才能够生成集合的所有排列,如果原始集合A中的元素不是有序的情况,字典序法将无法得到所有的排列结果,需要对原集合排序之后再执行生成算法,生成索引的全排列,避免了对原始集合的排序操作。

字典序算法还有一个优点,就是不受重复元素的影响。例如1224,交换中间的两个2,实际上得到的还是同一个排列,而字典序则是严格按照排列元素的大小关系来生成的。对于包含重复元素的输入集合,需要先将相同的元素放在一起,以集合A{a,d,b,c,d,b}为例,如果直接对其索引123456进行全排列,将不会得到想要的结果,这里将重复的元素放到相邻的位置,不同元素之间不一定有序,得到排列A'{a,d,d,b,b,c},然后将不同的元素,对应不同的索引值,生成索引排列122334,再执行全排列算法,即可得到最终结果。

 

https://www.cnblogs.com/sonnet/p/8996941.html

全排列(Permutation)

 

排列(英语:Permutation)是将相异物件或符号根据确定的顺序重排。每个顺序都称作一个排列。例如,从一到六的数字有720种排列,对应于由这些数字组成的所有不重复亦不阙漏的序列,例如4, 5, 6, 1, 2, 3 与1, 3, 5, 2, 4, 6。【From Wikipedia】

从n个相异元素中取出 k个元素,k个元素的排列数量为:

P

n

k

=

n!

(n−k)!

 

Pkn=n!(n−k)!

其中P意为Permutation(排列),!表示阶乘运算。全排列而取k为n,则结果为n!。

全排列生成算法

  1. 字典序法
    字典序,就是将元素按照字典的顺序(a-z, 1-9)进行排列。以字典的顺序作为比较的依据,可以比较出两个串的大小。比如 "1" < "13"<"14"<"153", 就是按每个数字位逐个比较的结果。对于一个串“123456789”, 可以知道最小的串是“123456789”,而最大的串“987654321”。这样针对这个串以字典序法生成全排列生成全排列,就是依次生成“123456789”->“123456798”->......->"987654312"->"987654321"这样的串。字典序法要求这一个与下一个有尽可能长的共同前缀,也即变化限制在尽可能短的后缀上。
  2. 邻位对换法
    该算法由Johnson-Trotter首先提出,是一个能快速生成全排列的算法。它的下一个全排列总是上一个全排列对换某相邻两位得到的。如果已知n-1个元素的排列,将n插入到排列的不同位置,就得到了n个元素的排列。用这种方法可以产生出任意n个元素的排列。这个方法有一个缺点:为了产生n个元素的排列,我们必须知道并存储所有n-1个元素的排列,然后才能产生出所有n阶排列。
  3. 递增进位制法
    这个算法是基于序列的递增进位制数[3]。递增进位制数是指数字的进制随着位数的递增而递增。一般情况下,数字最右边的进制是2,次右边的进制是3,以此类推。n位递增进位制数一共包含n!个数字,所以它可以与全排列生成算法结合在一起。
  4. 递减进位制法

该方法与递增进位制法的原理相似,不同的是它定义的“递减进位制数”是数字的进制随着位数的递增而递减。这种进制一般最左边的进制是2,次左边的进制是3。其余原理与递增进位制法基本相同。

---

###Python实现

#####字典序法

###### 非递归算法

设P是集合{1,2,……n-1,n}的一个全排列:P=P1P2……Pj-1PjPj+1……Pn(1≤P1,P2,……,Pn≤n-1)

1.从排列的右端开始,找出第一个比右边数字小的数字的序号j,即j=max{i|Pi&lt;Pi+1,i>j}在Pj的右边的数字中,

找出所有比Pj大的数字中最小的数字Pk,即k=min{i|Pi>Pj,i>j}

2.交换Pi,Pk

3.再将排列右端的递减部分Pj+1Pj+2……Pn倒转,因为j右端的数字是降序,所以只需要其左边和右边的交换,直到中间,因此可以得到一个新的排列P'=P1P2……Pj-1PkPn……Pj+2Pj+1

###### 代码

#!/usr/bin/env python2

# -*- coding: utf-8 -*-

"""

@author: gsharp

"""

def Swap(n,a,b):

    n[a],n[b] = n[b],n[a]

    return None

def Reverse(n,begin):

    if len(n) > begin:

        i = begin

        j = len(n)-1

        while i < j:

            Swap(n,i,j)

            i += 1

            j -= 1

    return n

 

def FindMin(n,i):

    j = len(n)-1

    k = i + 1

    while j > i:

        if n[j] > n[i] and n[j] < n[k]:

            k = j

        j -= 1

    return k

 

def Permut(n):

    count = 0

    j = len(n) -1  

    if j < 1:

        return n

    else :

        print n

        count += 1

        while j >= 1:

            i = j - 1

            if n[i] < n [j] :

                k = FindMin(n,i)

                Swap (n,i,k)

                Reverse (n,j)

                j = len(n) - 1

                count += 1

                print n

            else :

                j -= 1

    print count

 

n =[1,2,3,4,5,6]

Permut(n)

注意:

  1. 这里只能对于具有可比较值的列表排序,对于如【'~','!','@','#'】无法直接排序。
  2. 初始序列必须为最小序列,否则无法列出全部排列。可先使用快速排序来排序后作为输入。

 

https://blog.csdn.net/zhoufen12345/article/details/53560099

排列:从n个元素中任取m个元素,并按照一定的顺序进行排列,称为排列;

全排列:当n==m时,称为全排列;

 

比如:集合{ 1,2,3}的全排列为:

{ 1 2 3} 

{ 1 3 2 }

{ 2 1 3 }

{ 2 3 1 }

{ 3 2 1 }

{ 3 1 2 }

 

递归思想:

取出数组中第一个元素放到最后,即a[1]与a[n]交换,然后递归求a[n-1]的全排列

 

1)如果数组只有一个元素n=1,a={1} 则全排列就是{1}

2)如果数组有两个元素n=2,a={1,2} 则全排列是:

{2,1}--a[1]与a[2]交换。交换后求a[2-1]={2}的全排列,归结到1)

{1,2}--a[2]与a[2]交换。交换后求a[2-1]={1}的全排列,归结到1)

3)如果数组有三个元素n=3,a={1,2,3} 则全排列是

{{2,3},1}--a[1]与a[3]交换。后求a[3-1]={2,3}的全排列,归结到2)

{{1,3},2)--a[2]与a[3]交换。后求a[3-1]={1,3}的全排列,归结到2)

{{1,2},3)--a[3]与a[3]交换。后求a[3-1]={1,2}的全排列,归结到2)

...

依此类推。

利用python实现全排列的具体代码perm.py如下:

COUNT=0

def perm(n,begin,end):

    global COUNT

    if begin>=end:

        print n

        COUNT +=1

    else:

        i=begin

        for num in range(begin,end):

            n[num],n[i]=n[i],n[num]

            perm(n,begin+1,end)

            n[num],n[i]=n[i],n[num]

 

n=[1,2,3,4]

perm(n,0,len(n))

print COUNT

最后输出的结果如下:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值