作者:weapon

来源:https://zhuanlan.zhihu.com/p/54260935

起步

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

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

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

heapq 的使用

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

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

import heapqdata = [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 heapqdata = [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个元素的次小值 ;重复执行,得到一个有序序列,这个就是堆排序的过程。

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

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

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

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

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


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

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

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

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

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

def heapup(heap, startpos, pos):   # 如果是新增元素,startpos 传入 0while pos > startpos:parentpos = (pos - 1) // 2if heap[pos] < heap[parentpos]:heap[pos], heap[parentpos] = heap[parentpos], heap[pos]pos = parentposelse: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) >> 1parent = heap[parentpos]if newitem < parent:heap[pos] = parentpos = parentposcontinuebreakheap[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 emptyif heap:returnitem = heap[0]heap[0] = lastelt_siftup(heap, 0)return returnitemreturn lastelt

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

def _siftup(heap, pos):endpos = len(heap)startpos = posnewitem = 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 = childposchildpos = 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 等大体上都很类似。

     精 彩 文 章 

  • Python进阶:enum模块源码分析

  • 超好看的弦图,Python一行代码就能做

  • seaborn常用的10种数据分析图表

END

来和小伙伴们一起向上生长呀~~~

扫描下方二维码,添加小詹微信,可领取千元大礼包并申请加入 Python学习交流群,群内仅供学术交流,日常互动,如果是想发推文、广告、砍价小程序的敬请绕道!一定记得备注「交流学习」,我会尽快通过好友申请哦!

(添加人数较多,请耐心等待)

(扫码回复 1024  即可领取IT资料包)

Python 的 heapq 模块源码分析相关推荐

  1. python树状节点 可拖拽_Python 的 heapq 模块源码分析

    原文链接:Python 的 heapq 模块源码分析 起步 heapq 模块实现了适用于Python列表的最小堆排序算法. 堆是一个树状的数据结构,其中的子节点都与父母排序顺序关系.因为堆排序中的树是 ...

  2. Python 的 enum 模块源码分析

    ♚ 作者:weapon,闲来笑浮生悬笔一卷入毫端,朱绂临身可与言者不过二三. 博客:zhihu.com/people/hong-wei-peng 要想阅读这部分,需要对元类编程有所了解. 成员名不允许 ...

  3. Wifi模块—源码分析Wifi热点扫描2(Android P)

    一 前言 这次接着讲Wifi工程流程中的Wifi热点扫描过程部分的获取扫描结果的过程,也是Wifi扫描过程的延续,可以先看前面Wifi扫描的分析过程. Wifi模块-源码分析Wifi热点扫描(Andr ...

  4. Wifi模块—源码分析Wifi热点扫描(Android P)

    一 前言 这次接着讲Wifi工程流程中的Wifi热点查找过程,也是Wifi启动的过程延续,Wifi启动过程中会更新Wifi的状态,框架层也有相应广播发出,应用层接收到广播后开始进行热点的扫描.可以先看 ...

  5. Wifi模块—源码分析Wifi启动(Android P)

    一.前言 Android P在wifi这块改动挺大的,Wifi到AndoidO之后不再使用jni,所以AndroidP也一样不再使用jni来实现Java代码与本地的C/C++代码交互,而是使用HIDL ...

  6. Wifi模块—源码分析Wifi启动2(Android P)

    一 前言 在上一篇分析了wifi启动的流程,从Android应用层一直分析到了Java框架层,这次我们接着往下走流程.如果没有看上一篇的建议先回头看看   Wifi模块-源码分析Wifi启动1(And ...

  7. dubbo源码分析系列——dubbo-cluster模块源码分析

    2019独角兽企业重金招聘Python工程师标准>>> 模块功能介绍 该模块的使用介绍请参考dubbo官方用户手册如下章节内容. 集群容错 负载均衡 路由规则 配置规则 注册中心参考 ...

  8. kafka源码愫读(5)、ReplicaManager模块源码分析

    1.ReplicaManager模块简介 replicaManager主要用来管理topic在本broker上的副本信息.并且读写日志的请求都是通过replicaManager进行处理的. 每个rep ...

  9. 【GitHub探索】python调试利器——pysnooper源码分析

    前言 这次又开了个新坑--GitHub探索,主要内容是试水当期GitHub上较火的repo 虽然top榜上各路新手教程跟经典老不死项目占据了大半江山,但清流总是会有的. 第一期就试水一下pysnoop ...

最新文章

  1. window上创建python3虚拟环境
  2. FPGA之道(54)状态机的设计
  3. jFreeChary初探
  4. 阅读笔记 1 火球 UML大战需求分析
  5. Clion 远程同步设置
  6. leetcode 312. Burst Balloons | 312. 戳气球(暴力递归->DP)
  7. 过滤器 和 拦截器 6个区别,别再傻傻分不清了
  8. HDU 1083 Courses 匹配
  9. mysql使用substring_index达到splite功能
  10. beyond——实验吧
  11. HIVE数据导入MYSQL实现方式
  12. 今年纽微特公司股东会不开了?
  13. android java 调试快捷键_最强Android studio 使用快捷键和调试技巧
  14. ibm x3850装oracle,Oracle数据库服务器:x3850 X5
  15. Azure- 使用 Helm 管理应用程序和包
  16. springboot-vue前后端分离登录
  17. 二、全国计算机三级数据库考试——理论知识总结(填空题)
  18. oracle雾化试图_ORACLE物化视图具体解释
  19. 目标跟踪算法--Camshift 和Meanshift
  20. Web页面显示随机签名

热门文章

  1. sysbench压测服务器及结果解读(重点)
  2. php+swoole
  3. Bug访问豆瓣403forbidden
  4. c语言数码管连续加一,各位大神,如何用C语言实现在数码管上实现1234同时亮
  5. python atan_Python
  6. MySQL 5.7建表时date类型提示默认值类型错误的问题处理
  7. java编写教师类输出_Java类和对象的区别和联系,超简单易懂!
  8. 事物与持久化_揭开Redis面纱,发布订阅、事务、安全、持久化
  9. windows2008开机占用多少内存_Android内存占用分析
  10. 【阿里云OSS】403错误,AccessDenied:The bucket you access does not belong to you.