python heapq_heapq --- 堆队列算法 — Python 3.8.7rc1 文档

理论¶

堆是通过数组来实现的,其中的元素从 0 开始计数,对于所有的 k 都有 a[k] <= a[2*k+1] 且 a[k] <= a[2*k+2]。 为了便于比较,不存在的元素被视为无穷大。 堆最有趣的特性在于 a[0] 总是其中最小的元素。

上面的特殊不变量是用来作为一场锦标赛的高效内存表示。 下面的数字是 k 而不是 a[k]:

0

1 2

3 4 5 6

7 8 9 10 11 12 13 14

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

在上面的树中,每个 k 单元都位于 2*k+1 和 2*k+2 之上。 体育运动中我们经常见到二元锦标赛模式,每个胜者单元都位于另两个单元之上,并且我们可以沿着树形图向下追溯胜者所遇到的所有对手。 但是,在许多采用这种锦标赛模式的计算机应用程序中,我们并不需要追溯胜者的历史。 为了获得更高的内存利用效率,当一个胜者晋级时,我们会用较低层级的另一条目来替代它,因此规则变为一个单元和它之下的两个单元包含三个不同条目,上方单元“胜过”了两个下方单元。

如果此堆的不变量始终受到保护,则序号 0 显然是最后的赢家。 删除它并找出“下一个”赢家的最简单算法方式是家某个输家(让我们假定是上图中的 30 号单元)移至 0 号位置,然后将这个新的 0 号沿树下行,不断进行值的交换,直到不变量重新建立。 这显然会是树中条目总数的对数。 通过迭代所有条目,你将得到一个 O(n log n) 复杂度的排序。

此排序有一个很好的特性就是你可以在排序进行期间高效地插入新条目,前提是插入的条目不比你最近取出的 0 号元素“更好”。 这在模拟上下文时特别有用,在这种情况下树保存的是所有传入事件,“胜出”条件是最小调度时间。 当一个事件将其他事件排入执行计划时,它们的调试时间向未来方向延长,这样它们可方便地入堆。 因此,堆结构很适宜用来实现调度器,我的 MIDI 音序器就是用的这个 :-)。

用于实现调度器的各种结构都得到了充分的研究,堆是非常适宜的一种,因为它们的速度相当快,并且几乎是恒定的,最坏的情况与平均情况没有太大差别。 虽然还存在其他总体而言更高效的实现方式,但其最坏的情况却可能非常糟糕。

堆在大磁盘排序中也非常有用。 你应该已经了解大规模排序会有多个“运行轮次”(即预排序的序列,其大小通常与 CPU 内存容量相关),随后这些轮次会进入合并通道,轮次合并的组织往往非常巧妙 1。 非常重要的一点是初始排序应产生尽可能长的运行轮次。 锦标赛模式是达成此目标的好办法。 如果你使用全部有用内存来进行锦标赛,替换和安排恰好适合当前运行轮次的条目,你将可以对于随机输入生成两倍于内存大小的运行轮次,对于模糊排序的输入还会有更好的效果。

另外,如果你输出磁盘上的第 0 个条目并获得一个可能不适合当前锦标赛的输入(因为其值要“胜过”上一个输出值),它无法被放入堆中,因此堆的尺寸将缩小。 被释放的内存可以被巧妙地立即重用以逐步构建第二个堆,其增长速度与第一个堆的缩减速度正好相同。 当第一个堆完全消失时,你可以切换新堆并启动新的运行轮次。 这样做既聪明又高效!

总之,堆是值得了解的有用内存结构。 我在一些应用中用到了它们,并且认为保留一个 'heap' 模块是很有意义的。 :-)

备注

当前时代的磁盘平衡算法与其说是巧妙,不如说是麻烦,这是由磁盘的寻址能力导致的结果。 在无法寻址的设备例如大型磁带机上,情况则相当不同,开发者必须非常聪明地(极为提前地)确保每次磁带转动都尽可能地高效(就是说能够最好地加入到合并“进程”中)。 有些磁带甚至能够反向读取,这也被用来避免倒带的耗时。 请相信我,真正优秀的磁带机排序看起来是极其壮观的,排序从来都是一门伟大的艺术! :-)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值