Python中的heapq模块源码详析


Posted in Python onJanuary 08, 2019

起步

这是一个相当实用的内置模块,但是很多人竟然不知道他的存在——笔者也是今天偶然看到的,哎……尽管如此,还是改变不了这个模块好用的事实

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

Python中的heapq模块源码详析

堆是一个树状的数据结构,其中的子节点都与父母排序顺序关系。因为堆排序中的树是满二叉树,因此可以用列表来表示树的结构,使得元素 N 的子元素位于 2N + 1 和 2N + 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个元素的次小值 ;重复执行,得到一个有序序列,这个就是堆排序的过程。

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

  • 如何由一个无序序列建立成一个堆?
  • 如何在输出堆顶元素之后,调整剩余元素,使之成为一个新的堆?
  • 新添加元素和,如何调整堆?

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

Python中的heapq模块源码详析

如上图所示,当堆顶 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 个元素 (即此无序序列对应的完全二叉树的最后一个非终端结点 )起 ,至第一个元素止,依次进行下沉:

Python中的heapq模块源码详析

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 模块中还有很多其他 heapreplace ,heappushpop 等大体上都很类似。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
python启动办公软件进程(word、excel、ppt、以及wps的et、wps、wpp)
Apr 09 Python
python list中append()与extend()用法分享
Mar 24 Python
wxPython窗口的继承机制实例分析
Sep 28 Python
深入解析Python中的descriptor描述器的作用及用法
Jun 27 Python
PYTHON 中使用 GLOBAL引发的一系列问题
Oct 12 Python
Python unittest 简单实现参数化的方法
Nov 30 Python
python读写csv文件并增加行列的实例代码
Aug 01 Python
python调用matplotlib模块绘制柱状图
Oct 18 Python
python使用多线程+socket实现端口扫描
May 28 Python
Django-Scrapy生成后端json接口的方法示例
Oct 06 Python
Python numpy大矩阵运算内存不足如何解决
Nov 19 Python
python实现图片批量压缩
Apr 24 Python
python使用PIL模块获取图片像素点的方法
Jan 08 #Python
python 获取图片分辨率的方法
Jan 08 #Python
Python 处理图片像素点的实例
Jan 08 #Python
Python实现查找最小的k个数示例【两种解法】
Jan 08 #Python
对Python闭包与延迟绑定的方法详解
Jan 07 #Python
python将控制台输出保存至文件的方法
Jan 07 #Python
对Python捕获控制台输出流的方法详解
Jan 07 #Python
You might like
PHP函数学习之PHP函数点评
2012/07/05 PHP
详解js异步文件加载器
2016/01/24 PHP
学习YUI.Ext 第六天--关于树TreePanel(Part 1)
2007/03/10 Javascript
javascript 一些用法小结
2009/09/11 Javascript
基于Asp.net与Javascript控制的日期控件
2010/05/22 Javascript
Javascript this 的一些学习总结
2012/08/02 Javascript
浅析jQuery1.8的几个小变化
2013/12/10 Javascript
js跨域问题浅析及解决方法优缺点对比
2014/11/08 Javascript
巧用jQuery选择器提高写表单效率的方法
2016/08/19 Javascript
BootStrap中Table分页插件使用详解
2016/10/09 Javascript
用Angular实时获取本地Localstorage数据,实现一个模拟后台数据登入的效果
2016/11/09 Javascript
JavaScript实现同一个页面打开多张图片
2016/12/29 Javascript
详解JavaScript 中getElementsByName在IE中的注意事项
2017/02/21 Javascript
详解Vue路由History mode模式中页面无法渲染的原因及解决
2017/09/28 Javascript
js实现随机点名系统(实例讲解)
2017/10/18 Javascript
在Vue组件上动态添加和删除属性方法
2018/02/23 Javascript
浅谈angular2子组件的事件传递(任意组件事件传递)
2018/09/30 Javascript
基于layui内置模块(element常用元素的操作)
2019/09/20 Javascript
解决layui动态加载复选框无法选中的问题
2019/09/20 Javascript
vue-cli+webpack项目打包到服务器后,ttf字体找不到的解决操作
2020/08/28 Javascript
[44:50]DOTA2上海特级锦标赛B组小组赛#2 VG VS Fnatic第二局
2016/02/26 DOTA
python读写二进制文件的方法
2015/05/09 Python
Python爬虫实现网页信息抓取功能示例【URL与正则模块】
2017/05/18 Python
python实现关键词提取的示例讲解
2018/04/28 Python
python中报错&quot;json.decoder.JSONDecodeError: Expecting value:&quot;的解决
2019/04/29 Python
详解如何用TensorFlow训练和识别/分类自定义图片
2019/08/05 Python
使用python写的opencv实时监测和解析二维码和条形码
2019/08/14 Python
Python matplotlib生成图片背景透明的示例代码
2019/08/30 Python
python__new__内置静态方法使用解析
2020/01/07 Python
Python常用库大全及简要说明
2020/01/17 Python
python异常处理之try finally不报错的原因
2020/05/18 Python
python实现简单贪吃蛇游戏
2020/09/29 Python
Flesh Beauty官网:露华浓集团旗下彩妆品牌
2021/02/15 全球购物
惹女朋友生气检讨书
2015/05/06 职场文书
2016庆祝教师节新闻稿
2015/11/25 职场文书
使用springboot暴露oracle数据接口的问题
2021/05/07 Oracle