资讯专栏INFORMATION COLUMN

Python 的 heapq 模块源码分析

CoderBear / 3020人阅读

摘要:原文链接起步模块实现了适用于列表的最小堆排序算法。本文内容将分为三个部分,第一个部分简单介绍模块的使用第二部分回顾堆排序算法第三部分分析中的实现。总结堆排序结合图来理解还是比较好理解的。

原文链接:https://www.hongweipeng.com/i...

起步

heapq 模块实现了适用于Python列表的最小堆排序算法。

堆是一个树状的数据结构,其中的子节点都与父母排序顺序关系。因为堆排序中的树是满二叉树,因此可以用列表来表示树的结构,使得元素 N 的子元素位于 2N + 12N + 2 的位置(对于从零开始的索引)。

本文内容将分为三个部分,第一个部分简单介绍 heapq 模块的使用;第二部分回顾堆排序算法;第三部分分析heapq中的实现。

heapq 的使用

创建堆有两个基本的方法:heappush()heapify(),取出堆顶元素用 heappop()

heappush() 是用来向已有的堆中添加元素,一般从空列表开始构建:

import heapq

data = [97, 38, 27, 50, 76, 65, 49, 13]
heap = []

for n in data:
    heapq.heappush(heap, n)

print("pop:", heapq.heappop(heap)) # pop: 13
print(heap) # [27, 50, 38, 97, 76, 65, 49]

如果数据已经在列表中,则使用 heapify() 进行重排:

import heapq

data = [97, 38, 27, 50, 76, 65, 49, 13]

heapq.heapify(data)

print("pop:", heapq.heappop(data)) # pop: 13
print(data) # [27, 38, 49, 50, 76, 65, 97]
回顾堆排序算法

堆排序算法基本思想是:将无序序列建成一个堆,得到关键字最小(或最大的记录;输出堆顶的最小 (大)值后,使剩余的 n-1 个元素 重又建成一个堆,则可得到n个元素的次小值 ;重复执行,得到一个有序序列,这个就是堆排序的过程。

堆排序需要解决两个问题:

如何由一个无序序列建立成一个堆?

如何在输出堆顶元素之后,调整剩余元素,使之成为一个新的堆?

新添加元素和,如何调整堆?

先来看看第二个问题的解决方法。采用的方法叫“筛选”,当输出堆顶元素之后,就将堆中最后一个元素代替之;然后将根结点值与左、右子树的根结点值进行比较 ,并与其中小者进行交换;重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为“筛选”。

如上图所示,当堆顶 13 输出后,将堆中末尾的 97 替代为堆顶,然后堆顶与它的子节点 38 和 27 中的小者交换;元素 97 在新的位置上在和它的子节点 65 和 49 中的小者交换;直到元素97成为叶节点,就得到了新的堆。这个过程也叫 下沉

让堆中位置为 pos 元素进行下沉的如下:

def heapdown(heap, pos):
    endpos = len(heap)
    while pos < endpos:
        lchild = 2 * pos + 1
        rchild = 2 * pos + 2
        if lchild >= endpos: # 如果pos已经是叶节点,退出循环
            break
        childpos = lchild   # 假设要交换的节点是左节点
        if rchild < endpos and heap[childpos] > heap[rchild]:
            childpos = rchild
            
        if heap[pos] < heap[childpos]: # 如果节点比子节点都小,退出循环
            break
        heap[pos], heap[childpos]  = heap[childpos], heap[pos]  # 交换
        pos = childpos

再来看看如何解决第三个问题:新添加元素和,如何调整堆?这个的方法正好与 下沉 相反,首先将新元素放置列表的最后,然后新元素与其父节点比较,若比父节点小,与父节点交换;重复过程直到比父节点大或到根节点。这个过程使得元素从底部不断上升,从下至上恢复堆的顺序,称为 上浮

将位置为 pos 进行上浮的代码为:

def heapup(heap, startpos, pos):   # 如果是新增元素,startpos 传入 0
    while pos > startpos:
        parentpos = (pos - 1) // 2
        if heap[pos] < heap[parentpos]:
            heap[pos], heap[parentpos] = heap[parentpos], heap[pos]
            pos = parentpos
        else:
            break

第一个问题:如何由一个无序序列建立成一个堆?从无序序列的第 n/2 个元素 (即此无序序列对应的完全二叉树的最后一个非终端结点 )起 ,至第一个元素止,依次进行下沉:

for i in reversed(range(len(data) // 2)):
    heapdown(data, i)
heapq 源码分析

添加新元素到堆中的 heappush() 函数:

def heappush(heap, item):
    """Push item onto heap, maintaining the heap invariant."""
    heap.append(item)
    _siftdown(heap, 0, len(heap)-1)

把目标元素放置列表最后,然后进行上浮。尽管它命名叫 down ,但这个过程是上浮的过程,这个命名也让我困惑,后来我才知道它是因为元素的索引不断减小,所以命名 down 。下沉的过程它也就命名为 up 了。

def _siftdown(heap, startpos, pos):
    newitem = heap[pos]
    # Follow the path to the root, moving parents down until finding a place
    # newitem fits.
    while pos > startpos:
        parentpos = (pos - 1) >> 1
        parent = heap[parentpos]
        if newitem < parent:
            heap[pos] = parent
            pos = parentpos
            continue
        break
    heap[pos] = newitem

一样是通过 newitem 不断与父节点比较。不一样的是这里缺少了元素交换的过程,而是计算出新元素最后所在的位置 pos 并进行的赋值。显然这是优化后的代码,减少了不断交换元素的冗余过程。

再来看看输出堆顶元素的函数 heappop():

def heappop(heap):
    """Pop the smallest item off the heap, maintaining the heap invariant."""
    lastelt = heap.pop()    # raises appropriate IndexError if heap is empty
    if heap:
        returnitem = heap[0]
        heap[0] = lastelt
        _siftup(heap, 0)
        return returnitem
    return lastelt

通过 heap.pop() 获得列表中的最后一个元素,然后替换为堆顶 heap[0] = lastelt ,再进行下沉:

def _siftup(heap, pos):
    endpos = len(heap)
    startpos = pos
    newitem = heap[pos]
    # Bubble up the smaller child until hitting a leaf.
    childpos = 2*pos + 1    # 左节点,默认替换左节点
    while childpos < endpos:
        # Set childpos to index of smaller child.
        rightpos = childpos + 1  # 右节点
        if rightpos < endpos and not heap[childpos] < heap[rightpos]:
            childpos = rightpos  # 当右节点比较小时,应交换的是右节点
        # Move the smaller child up.
        heap[pos] = heap[childpos]
        pos = childpos
        childpos = 2*pos + 1
    # The leaf at pos is empty now.  Put newitem there, and bubble it up
    # to its final resting place (by sifting its parents down).
    heap[pos] = newitem
    _siftdown(heap, startpos, pos)

这边的代码将准备要下沉的元素视为新元素 newitem ,将其当前的位置 pos 视为空位置,由其子节点中的小者进行取代,反复如此,最后会在叶节点留出一个位置,这个位置放入 newitem ,再让新元素进行上浮。

再来看看让无序数列重排成堆的 heapify() 函数:

def heapify(x):
    """Transform list into a heap, in-place, in O(len(x)) time."""
    n = len(x)
    for i in reversed(range(n//2)):
        _siftup(x, i)

这部分就和理论上的一致,从最后一个非叶节点 (n // 2) 到根节点为止,进行下沉。

总结

堆排序结合图来理解还是比较好理解的。这种数据结构常用于优先队列(标准库Queue的优先队列用的就是堆)。 heapq 模块中还有很多其他 heapreplaceheappushpop 等大体上都很类似。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/42933.html

相关文章

  • Python基础之(十)模块

    摘要:是回调函数,当链接服务器和相应数据传输完毕时触发本函数可选。仅仅是针对的,在中,已经没有这个模块了,取代它的是。由于以流式读取文件,从而速度较快,切少占用内存,但是操作上稍复杂,需要用户实现回调函数。 编写模块 模块是程序 模块就是一个扩展名为.py的Python程序。 编写模块 #!/usr/bin/env python # coding=utf-8 lang = python 引...

    jlanglang 评论0 收藏0
  • python之排序操作及heapq模块

    摘要:内置的模块接下来我们一一介绍。小伙伴们有没有想我为何介绍这个模块,并且和排序放在一起呢,其实在很多时候我们需要找序列中的前几个最大值或者最小值,使用此模块中的方法是最好不过的了。 说到排序,很多人可能第一想到的就是sorted,但是你可能不知道python中其实还有还就中方法哟,并且好多种场景下效率都会比sorted高。那么接下来我就依次来介绍我所知道的排序操作。sorted(iter...

    dongfangyiyu 评论0 收藏0
  • Python 列表推导及优先级队列实现

    摘要:主要介绍列表列表推导有关的话题,最后演示如何用列表实现一个优先级队列。笛卡尔积列表推导还可以生成两个或以上的可迭代类型的笛卡尔积。两个优先级相同的元素和,操作按照它们被插入到队列的顺序返回。变量的作用是保证同等优先级元素的正确排序。 这一篇是《流畅的 python》读书笔记。主要介绍列表、列表推导有关的话题,最后演示如何用列表实现一个优先级队列。 Python 内置序列类型 Pytho...

    darkerXi 评论0 收藏0
  • Python每日一练0006

    摘要:问题在某个集合中找到最大或最小的个元素解决方案使用模块例如此外,这两个函数都可以接受作为参数,例如输出为讨论根据官方文档对的介绍可以了解到提供了堆数据结构的实现,并且实现方式是小顶堆,也就是说每次的时候取出的是最小的元素首先使用将一个列 问题 在某个集合中找到最大或最小的N个元素 解决方案 使用heapq模块 heapq.nlargest(n, iterable, key=None)h...

    Batkid 评论0 收藏0
  • 4-array/heapq/queue模块

    摘要:定义了一个非常类似的模块,其函数接受两个参数,第一个参数是预先定义好的类型,第二个参数,一般为一个序列。很少见到代码输出是中实现堆排序的模块。这里主要看一下优先级队列定义优先级比较输出 array array 定义了一个非常类似list的模块,其array 函数接受两个参数,第一个参数是预先定义好的类型,第二个参数,一般为一个序列。 很少见到代码: import array a = ...

    forrest23 评论0 收藏0

发表评论

0条评论

CoderBear

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<