6.2 全排列Heap算法

算法原理

  本文的阅读,需要读者有置换知识的基础。可以阅读我的博文6.4 置换基本概念

  上述的排列树算法虽然也适用于全排列。但是全排列有更好的算法,这个算法就是大名鼎鼎的Heap算法Heap’s algorithm。注意,这里的Heap不是堆的意思,而是计算机科学家B. R. Heap的姓氏。
  Heap算法是一种递归算法。他的想法就是用互换生成整个凯莱图。其基本算法是这样的,先把前n-1项使用递归方法去互换。一系列互换完成,再与最后一个元素互换。具体的置换原理非常难,网上没有讲清楚的,我这次多用几张图画清楚吧,以ABC三个元素的排列,先看看位置变化:
在这里插入图片描述
  上面的图什么意思呢?在减号前面是进入节点时的排列,减号后面是程序流程离开节点后的排列。
  这很难看出规律,如果知道置换知识可能更容易理解。我们改成置换表达式再看看:
在这里插入图片描述

  在置换图里,因为下层递归完成后,会再执行一次置换,而这次置换会影响同级的下一个节点,所以我把置换放入了平级箭头上。因为最后还执行了一次(1 3),所以我画了一个指向根节点的箭头。我们知道对于3个元素的置换,(1 2)与(1 3)交替就可以遍历完,这在凯莱图里是一个六边形。三元素的置换不足以剖析Heap算法的精妙之处。
  因为三元素的置换,会让人误以为,每个元素和最后一个元素交换就行了。但是事实不是这样,起码两个元素的交换不是这样,比如最底层的 ( 1    2 ) (1\;2) (12)后面没有再进行一个 ( 1    2 ) (1\;2) (12)置换。下面这幅图是四个元素的全排列:
在这里插入图片描述
  接下来我重点讲讲为什么奇偶处理不一样。如果把四元素的置换换成四个左陪集效果会更明显:
在这里插入图片描述
  而大循环的起点是这样的:
在这里插入图片描述
  对于n为偶数的对称群 S n S_n Sn来说,有这么一个规律:其子群 S n − 1 S_{n-1} Sn1的每个左陪集里有 e , ( 1    n ) , ( 1    2    n ) , ( 1    2    3    n ) , … , ( 1    2    3 …    n ) e,(1\;n),(1\;2\;n),(1\;2\;3\;n),\dots,(1\;2\;3\dots\;n) e,(1n),(12n),(123n),,(123n)这些元素。注意,偶数不会还原为恒等置换e,会残留一个 ( 1    2    3 …    n ) (1\;2\;3\dots\;n) (123n)这样的置换。那么如果放到更大的循环,例如进行五元素的排列,又会如何呢?看下图:在这里插入图片描述
  对于奇数来说而每个偶数的递归子程序会残留一个 ( 1    2    3 …    n − 1 ) (1\;2\;3\dots\;n-1) (123n1),与奇数的 ( 1    n ) (1\;n) (1n)恰好组成 ( 1    2    3 …    n ) (1\;2\;3\dots\;n) (123n)置换,而n个 ( 1    2    3 …    n ) (1\;2\;3\dots\;n) (123n)恰好变回恒等置换e。例如上图的 ( 1    2    3    4 ) (1\;2\;3\;4) (1234) ( 1    5 ) (1\;5) (15)组成 ( 1    2    3    4    5 ) (1\;2\;3\;4\;5) (12345),五个 ( 1    2    3    4    5 ) (1\;2\;3\;4\;5) (12345)恰好还原为e。所以Heap算法的核心原理就是偶数残留一个全错位置换( ( 1    2    3 …    n ) (1\;2\;3\dots\;n) (123n)),奇数再还原为e。这样一奇一偶,循环往复,其代码思想十分优美。

Python实现

def recursive(array, last, result):
    if last == 0:
        result.append(array[:])
        return

    for i in range(0, last + 1):
        recursive(array, last - 1, result)
        before = array[:]
        if last & 1 == 0:
            array[0], array[last] = array[last], array[0]
        else:
            array[i], array[last] = array[last], array[i]


def permutations(array):
    result = []
    recursive(array, len(array) - 1, result)
    return result


if __name__ == '__main__':
    result = permutations(['A', 'B', 'C', 'D'])
    for x in result:
        print(x)


  测试结果:

['A', 'B', 'C', 'D']
['B', 'A', 'C', 'D']
['C', 'A', 'B', 'D']
['A', 'C', 'B', 'D']
['B', 'C', 'A', 'D']
['C', 'B', 'A', 'D']
['D', 'B', 'C', 'A']
['B', 'D', 'C', 'A']
['C', 'D', 'B', 'A']
['D', 'C', 'B', 'A']
['B', 'C', 'D', 'A']
['C', 'B', 'D', 'A']
['D', 'A', 'C', 'B']
['A', 'D', 'C', 'B']
['C', 'D', 'A', 'B']
['D', 'C', 'A', 'B']
['A', 'C', 'D', 'B']
['C', 'A', 'D', 'B']
['D', 'A', 'B', 'C']
['A', 'D', 'B', 'C']
['B', 'D', 'A', 'C']
['D', 'B', 'A', 'C']
['A', 'B', 'D', 'C']
['B', 'A', 'D', 'C']
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

醒过来摸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值