【问题标题】:What is Python's heapq module?什么是 Python 的 heapq 模块?
【发布时间】:2013-11-27 13:45:50
【问题描述】:

我尝试了"heapq" 并得出结论,我的期望与我在屏幕上看到的不同。我需要有人解释它是如何工作的以及它在哪里有用。

来自Python Module of the Week这本书的2.2排序段下写的

如果您需要在添加和删除值时维护一个排序列表, 查看 heapq。通过使用 heapq 中的函数来添加或删除 列表中的项目,您可以使用维护列表的排序顺序 低开销。

这是我所做的和得到的。

import heapq
heap = []

for i in range(10):
    heap.append(i)

heap
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

heapq.heapify(heap)    
heapq.heappush(heap, 10)    
heap
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

heapq.heappop(heap)
0    
heap
[1, 3, 2, 7, 4, 5, 6, 10, 8, 9] <<< Why the list does not remain sorted?

heapq.heappushpop(heap, 11)
1
heap
[2, 3, 5, 7, 4, 11, 6, 10, 8, 9] <<< Why is 11 put between 4 and 6?

因此,正如您所见,“堆”列表根本没有排序,事实上,您添加和删除的项目越多,它就会变得越混乱。推送的值处于无法解释的位置。 到底是怎么回事?

【问题讨论】:

  • 断章取义,那句话是错误的。堆不维护排序列表;它维护一组值,以便可以在恒定时间内访问最小项,或在 O(lg n) 时间内删除。您可以通过反复从列表中删除最小的项目来检索排序列表。
  • 在跟踪报价后,我发现这只是一种误导。堆不维护排序列表,但它确实维护可用于创建排序列表的数据结构。它省略了检索列表必须销毁堆的细节,这是一个至关重要的细节。
  • l4mpi:我阅读了官方的 python 文档,但仍然不明白,你的建议是什么?:) chepner:这是误导,这就是我提出这个问题的原因。任何没有额外知识的人都会期望heapq 在阅读我提到的书中的相关内容后维护一个排序列表。
  • @l4mpi: 没必要这个苛刻;引用显然是错误的,可以理解的是会产生混乱。对于许多初学者来说,算法理论也可能相当枯燥。

标签: python data-structures heap python-module


【解决方案1】:

heapq 模块维护堆不变量,这与维护实际列表对象的排序顺序不同。

引用heapq documentation:

堆是二叉树,每个父节点的值都小于或等于其任何子节点。此实现使用数组,其中heap[k] &lt;= heap[2*k+1]heap[k] &lt;= heap[2*k+2] 用于所有k,从零开始计数元素。为了比较,不存在的元素被认为是无限的。堆的有趣属性是它的最小元素始终是根,heap[0]

这意味着找到最小元素非常有效(只需取heap[0]),这对于优先级队列来说非常有用。之后,接下来的 2 个值将大于(或等于)第一个,接下来的 4 个将大于它们的“父”节点,然后接下来的 8 个更大,等等。

您可以在Theory section of the documentation 中阅读有关数据结构背后理论的更多信息。您也可以观看this lecture from the MIT OpenCourseWare Introduction to Algorithms course,它对算法进行了一般性的解释。

堆可以非常有效地转回排序列表:

def heapsort(heap):
    return [heapq.heappop(heap) for _ in range(len(heap))]

只需从堆中弹出下一个元素。不过,使用 sorted(heap) 应该更快,因为 Python 排序使用的 TimSort 算法将利用堆中已经存在的部分排序。

如果您只对最小值或第一个n 最小值感兴趣,则可以使用堆,特别是如果您对这些值持续感兴趣;添加新项目并删除最小的项目确实非常有效,比每次添加值时都重新排列列表更有效。

【讨论】:

  • 也许我误解了,但是:“之后,接下来的 2 个值将大于(或等于)第一个值,然后接下来的 4 个值将大于前 3 个,然后接下来的 8 个更大,以此类推。” – 作为反例:[1, 5, 9, 7, 15, 10, 11] 是一个有效的二进制最小堆,但例如7(层次结构中的第三级)仍然小于9(层次结构中的第二级)。堆中的有序属性仅适用于父子遍历,不一定适用于“阿姨-侄女”关系。
  • @DanielAndersson:是的,那句话被过度简化了,通过简化,现在基本上是错误的。感谢您指出这一点!
  • 我觉得你的用法不太合适,heapsort(range(100, 0 , -1)),结果就像 100, 1, 2, 3 ... 98, 99. 修复它,在你真正弹出项目之前尝试堆积一次:def heapsort(heap): heapq.heapify(heap) return [heapq.heappop(heap) for _ in range(len(heap))]
  • @AlbertLee: heap 被假定为一个合适的堆。如果您需要先调用heapify(),那么它不是一个合适的堆;你没有更新堆不变量。
  • @MartijnPieters,我想你可以把你的函数名改成:generate_sorted_array_from_heap 而不是 heapysort,你同意吗?
【解决方案2】:

你的书错了!正如你所展示的,堆不是排序列表(尽管排序列表是堆)。什么是堆?引用 Skiena 的算法设计手册

堆是一种简单而优雅的数据结构,可以有效地支持优先级队列操作 insert 和 extract-min。它们的工作原理是在元素集合上保持偏序,该偏序弱于排序顺序(因此可以有效地维护)但比随机顺序强(因此可以快速识别最小元素)。

与排序列表相比,堆遵循较弱的条件堆不变量。在定义它之前,首先考虑为什么放松条件可能有用。答案是较弱的条件更容易维护。你可以用堆做更少的事情,但你可以做到更快

一个堆有三个操作:

  1. 求最小值为 O(1)
  2. 插入 O(log n)
  3. 删除最小 O(log n)

插入排序的关键是 O(log n),它优于 O(n)。

什么是堆不变量? “父母支配孩子的二叉树”。也就是说,“p ≤ c for all children c of p”。 Skiena 用图片说明并继续演示在保持不变性的同时插入元素的算法。如果你想了一会儿,你可以自己发明它们。 (提示:它们被称为冒泡和冒泡)

好消息是,包含电池的 Python 在 heapq 模块中为您实现了一切。它没有定义堆类型(我认为它更容易使用),而是将它们作为列表中的辅助函数提供。

道德:如果您使用排序列表编写算法,但只从一端检查和删除,那么您可以使用堆来提高算法的效率。

对于堆数据结构有用的问题,请阅读https://projecteuler.net/problem=500

【讨论】:

  • 如何比较 hashtable(Python 中的字典)和堆表进行插入/删除的效率?我知道哈希表在最好的情况下做 O(1),在最坏的情况下做 O(n)。 O(log n) 是堆的最坏情况还是平均情况??
  • @enaJ:你不用比较它们:dict(或set)根本不按值排序。
【解决方案3】:

堆数据结构实现存在一些误区。 heapq 模块实际上是 binary heap 实现的变体,其中堆元素存储在列表中,如下所述:https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation

引用维基百科:

堆通常用数组来实现。任何二叉树都可以存储在数组中,但由于二叉堆始终是完整的二叉树,因此可以紧凑地存储。指针不需要空间;相反,每个节点的父节点和子节点可以通过数组索引的算术找到。

下面的这张图片应该可以帮助您感受堆的树和列表表示之间的区别,并且(注意,这是一个最大堆,与通常的最小堆相反! ):

一般来说,堆数据结构与排序列表的不同之处在于它牺牲了一些关于任何特定元素是否比任何其他元素更大或更小的信息。堆只能说,这个特定的元素比它的父元素小,比它的孩子大。数据结构存储的信息越少,修改它所需的时间/内存就越少。比较堆和排序数组之间一些操作的复杂度:

        Heap                  Sorted array
        Average  Worst case   Average   Worst case

Space   O(n)     O(n)         O(n)      O(n)

Search  O(n)     O(n)         O(log n)  O(log n)

Insert  O(1)     O(log n)     O(n)      O(n)

Delete  O(log n) O(log n)     O(n)      O(n)

【讨论】:

    【解决方案4】:

    我知道这是一个较老的问题,但 OP 只是错过了答案,并附有图表并解释了为什么以线性方式列出时排序顺序看起来不正常。

    (所以我不讨论优化、效率等问题。我正在回答视觉排序、OP 问题的结构)

    他在 pymotw.com,但如果他只是得到: https://pymotw.com/2/heapq/

    " 最小堆要求父级小于或等于其子级"

    所以想想树,想想金字塔。

    这也不是一个坏链接 https://medium.com/basecs/learning-to-love-heaps-cef2b273a238

    所以每个父母都有一个二孩政策。而且孩子们也只能有两个子元素。

    它的美妙之处在于,孩子总是小于或等于(堆最大)他们的父母或大于或等于他们的父母(堆最小)。

    heap-max 或 heap-min(会导致混淆)指的是最顶层的元素,或者如果是线性的,

    堆[0]。是否表示最大值作为开始或最小值作为开始。

    我将尽可能不考虑数学。

    所以(数字是索引)

    heap[0] 有两个孩子。堆[1] 和堆[2]。

    heap[1] 孩子将是 heap[3] 和 heap[4]

    heap[2] 孩子将是 heap[5] 和 heap[6]

    heap[3] 孩子将是 heap[7] 和 heap[8]

    heap[4] 孩子将是 heap[9] 和 heap[10]

    等等。

    所以,问题,

    [2, 3, 5, 7, 4, 11, 6, 10, 8, 9] &lt;&lt;&lt; Why is 11 put between 4 and 6?

    因为值 11 存储在索引 5 中。索引 5 是索引 2 的子索引,其值为 3。值 4(索引 4)是索引 1 的子索引

    它是从最小开始排序的,只是在以线性方式检查时看起来并不像。

    parent -> child 
    
    [0] -> [0] is 2
    -
    [0] -> [1] is 3
    [0] -> [2] is 5
    -
    [1] -> [3] is 7
    [1] -> [4] is 4
    [2] -> [5] is 11  <-- between 4 and 6
    [2] -> [6] is 6
    

    所以....又是这样。这是真的。 “最小堆要求父级小于或等于其子级”

    让自己发疯,把它画到最极致……这仍然是真实的。

    (曾经写过这些东西,然后等着被某个博士后压扁吗?)

    所以让我们弹出第一个元素并像普通列表或队列一样做

    [0] -> [0] is 3
    -
    [0] -> [1] is 5
    [0] -> [2] is 7
    -
    [1] -> [3] is 4
    [1] -> [4] is 11  
    

    让我们停下来。

    索引 1 的值为 5。索引 3,它的孩子的值为 4 并且更小....规则被打破。堆被重新排序以维持关系。所以它基本上不会看起来排序,并且在弹出值之前它看起来不会像之前的迭代一样。

    有一些方法可以重新排序节点,第二篇文章讨论了它们。我只是想具体回答一下这个问题。

    【讨论】:

      猜你喜欢
      • 2011-04-13
      • 2012-01-27
      • 2019-05-01
      • 2019-07-02
      • 1970-01-01
      • 1970-01-01
      • 2014-07-01
      • 2022-12-09
      • 2018-07-28
      相关资源
      最近更新 更多