Python实现二叉堆


Posted in Python onFebruary 03, 2016

优先队列的二叉堆实现

在前面的章节里我们学习了“先进先出”(FIFO)的数据结构:队列(Queue)。队列有一种变体叫做“优先队列”(Priority Queue)。优先队列的出队(Dequeue)操作和队列一样,都是从队首出队。但在优先队列的内部,元素的次序却是由“优先级”来决定:高优先级的元素排在队首,而低优先级的元素则排在后面。这样,优先队列的入队(Enqueue)操作就比较复杂,需要将元素根据优先级尽量排到队列前面。我们将会发现,对于下一节要学的图算法中的优先队列是很有用的数据结构。

我们很自然地会想到用排序算法和队列的方法来实现优先队列。但是,在列表里插入一个元素的时间复杂度是O(n),对列表进行排序的时间复杂度是O(nlogn)。我们可以用别的方法来降低时间复杂度。一个实现优先队列的经典方法便是采用二叉堆(Binary Heap)。二叉堆能将优先队列的入队和出队复杂度都保持在O(logn)

二叉堆的有趣之处在于,其逻辑结构上像二叉树,却是用非嵌套的列表来实现。二叉堆有两种:键值总是最小的排在队首称为“最小堆(min heap)”,反之,键值总是最大的排在队首称为“最大堆(max heap)”。在这一节里我们使用最小堆。

二叉堆的操作

二叉堆的基本操作定义如下:

  1. BinaryHeap():创建一个空的二叉堆对象
  2. insert(k):将新元素加入到堆中
  3. findMin():返回堆中的最小项,最小项仍保留在堆中
  4. delMin():返回堆中的最小项,同时从堆中删除
  5. isEmpty():返回堆是否为空
  6. size():返回堆中节点的个数
  7. buildHeap(list):从一个包含节点的列表里创建新堆

下面所示代码是二叉堆的示例。可以看到无论我们以哪种顺序把元素添加到堆里,每次都是移除最小的元素。我们接下来要来实现这个过程。

from pythonds.trees.binheap import BinHeap

bh = BinHeap()
bh.insert(5)
bh.insert(7)
bh.insert(3)
bh.insert(11)

print(bh.delMin())

print(bh.delMin())

print(bh.delMin())

print(bh.delMin())

为了更好地实现堆,我们采用二叉树。我们必须始终保持二叉树的“平衡”,就要使操作始终保持在对数数量级上。平衡的二叉树根节点的左右子树的子节点个数相同。在堆的实现中,我们采用“完全二叉树”的结构来近似地实现“平衡”。完全二叉树,指每个内部节点树均达到最大值,除了最后一层可以只缺少右边的若干节点。图 1 所示是一个完全二叉树。

Python实现二叉堆

图 1:完全二叉树

有意思的是我们用单个列表就能实现完全树。我们不需要使用节点,引用或嵌套列表。因为对于完全二叉树,如果节点在列表中的下标为 p,那么其左子节点下标为 2p,右节点为 2p+1。当我们要找任何节点的父节点时,可以直接使用 python 的整除。如果节点在列表中下标为n,那么父节点下标为n//2.图 2 所示是一个完全二叉树和树的列表表示法。注意父节点与子节点之间 2p 与 2p+1 的关系。完全树的列表表示法结合了完全二叉树的特性,使我们能够使用简单的数学方法高效地遍历一棵完全树。这也使我们能高效实现二叉堆。

堆次序的性质

我们在堆里储存元素的方法依赖于堆的次序。所谓堆次序,是指堆中任何一个节点 x,其父节点 p 的键值均小于或等于 x 的键值。图 2 所示是具备堆次序性质的完全二叉树。

Python实现二叉堆

图 2:完全树和它的列表表示法

二叉堆操作的实现

接下来我们来构造二叉堆。因为可以采用一个列表保存堆的数据,构造函数只需要初始化一个列表和一个currentSize来表示堆当前的大小。Listing 1 所示的是构造二叉堆的 python 代码。注意到二叉堆的heaplist并没有用到,但为了后面代码可以方便地使用整除,我们仍然保留它。

Listing 1

class BinHeap:
  def __init__(self):
    self.heapList = [0]
    self.currentSize = 0

我们接下来要实现的是insert方法。首先,为了满足“完全二叉树”的性质,新键值应该添加到列表的末尾。然而新键值简单地添加在列表末尾,显然无法满足堆次序。但我们可以通过比较父节点和新加入的元素的方法来重新满足堆次序。如果新加入的元素比父节点要小,可以与父节点互换位置。图 3 所示的是一系列交换操作来使新加入元素“上浮”到正确的位置。

Python实现二叉堆

图 3:新节点“上浮”到其正确位置

当我们让一个元素“上浮”时,我们要保证新节点与父节点以及其他兄弟节点之间的堆次序。当然,如果新节点非常小,我们仍然需要将它交换到其他层。事实上,我们需要不断交换,直到到达树的顶端。Listing 2 所示的是“上浮”方法,它把一个新节点“上浮”到其正确位置来满足堆次序。这里很好地体现了我们之前在headlist中没有用到的元素 0 的重要性。这样只需要做简单的整除,将当前节点的下标除以 2,我们就能计算出任何节点的父节点。

在Listing 3 中,我们已经可以写出insert方法的代码。insert里面很大一部分工作是由percUp函数完成的。当树添加新节点时,调用percUp就可以将新节点放到正确的位置上。

Listing 2

def percUp(self,i):
  while i // 2 > 0:
   if self.heapList[i] < self.heapList[i // 2]:
     tmp = self.heapList[i // 2]
     self.heapList[i // 2] = self.heapList[i]
     self.heapList[i] = tmp
   i = i // 2

Listing 3

def insert(self,k):
  self.heapList.append(k)
  self.currentSize = self.currentSize + 1
  self.percUp(self.currentSize)

我们已经写好了insert方法,那再来看看delMin方法。堆次序要求根节点是树中最小的元素,因此很容易找到最小项。比较困难的是移走根节点的元素后如何保持堆结构和堆次序,我们可以分两步走。首先,用最后一个节点来代替根节点。移走最后一个节点保持了堆结构的性质。这么简单的替换,还是会破坏堆次序。那么第二步,将新节点“下沉”来恢复堆次序。图 4 所示的是一系列交换操作来使新节点“下沉”到正确的位置。

Python实现二叉堆

图 4:替换后的根节点下沉

为了保持堆次序,我们需将新的根节点沿着一条路径“下沉”,直到比两个子节点都小。在选择下沉路径时,如果新根节点比子节点大,那么选择较小的子节点与之交换。Listing 4 所示的是新节点下沉所需的percDownminChild方法的代码。

Listing 4

def percDown(self,i):
  while (i * 2) <= self.currentSize:
    mc = self.minChild(i)
    if self.heapList[i] > self.heapList[mc]:
      tmp = self.heapList[i]
      self.heapList[i] = self.heapList[mc]
      self.heapList[mc] = tmp
    i = mc

def minChild(self,i):
  if i * 2 + 1 > self.currentSize:
    return i * 2
  else:
    if self.heapList[i*2] < self.heapList[i*2+1]:
      return i * 2
    else:
      return i * 2 + 1

Listing 5 所示的是delMin操作的代码。可以看到比较麻烦的地方由一个辅助函数来处理,即percDown

Listing 5

def delMin(self):
  retval = self.heapList[1]
  self.heapList[1] = self.heapList[self.currentSize]
  self.currentSize = self.currentSize - 1
  self.heapList.pop()
  self.percDown(1)
  return retval

关于二叉堆的最后一部分便是找到从无序列表生成一个“堆”的方法。我们首先想到的是,将无序列表中的每个元素依次插入到堆中。对于一个排好序的列表,我们可以用二分搜索找到合适的位置,然后在下一个位置插入这个键值到堆中,时间复杂度为O(logn)。另外插入一个元素到列表中需要将列表的一些其他元素移动,为新节点腾出位置,时间复杂度为O(n)。因此用insert方法的总开销是O(nlogn)。其实我们能直接将整个列表生成堆,将总开销控制在O(n)。Listing 6 所示的是生成堆的操作。

Listing 6

def buildHeap(self,alist):
  i = len(alist) // 2
  self.currentSize = len(alist)
  self.heapList = [0] + alist[:]
  while (i > 0):
    self.percDown(i)
    i = i - 1

Python实现二叉堆

图 5:将列表[ 9, 6, 5, 2, 3]生成一个二叉堆

图 5 所示的是利用buildHeap方法将最开始的树[ 9, 6, 5, 2, 3]中的节点移动到正确的位置时所做的交换操作。尽管我们从树中间开始,然后回溯到根节点,但percDown方法保证了最大子节点总是“下沉”。因为堆是完全二叉树,任何在中间的节点都是叶节点,因此没有子节点。注意,当i=1时,我们从根节点开始下沉,这就需要进行大量的交换操作。可以看到,图 5 最右边的两颗树,首先 9 从根节点的位置移走,移到下一层级之后,percDown进一步检查它此时的子节点,保证它下降到不能再下降为止,即下降到正确的位置。然后进行第二次交换,9 和 3 的交换。由于 9 已经移到了树最底层的层级,便无法进一步交换了。比较一下列表表示法和图 5 所示的树表示法进行的一系列交换还是很有帮助的。

i = 2 [0, 9, 5, 6, 2, 3]
i = 1 [0, 9, 2, 6, 5, 3]
i = 0 [0, 2, 3, 6, 5, 9]

下列所示的代码是完全二叉堆的实现。

def insert(self,k):
   self.heapList.append(k)
   self.currentSize = self.currentSize + 1
   self.percUp(self.currentSize)

  def percDown(self,i):
   while (i * 2) <= self.currentSize:
     mc = self.minChild(i)
     if self.heapList[i] > self.heapList[mc]:
       tmp = self.heapList[i]
       self.heapList[i] = self.heapList[mc]
       self.heapList[mc] = tmp
     i = mc

  def minChild(self,i):
   if i * 2 + 1 > self.currentSize:
     return i * 2
   else:
     if self.heapList[i*2] < self.heapList[i*2+1]:
       return i * 2
     else:
       return i * 2 + 1

  def delMin(self):
   retval = self.heapList[1]
   self.heapList[1] = self.heapList[self.currentSize]
   self.currentSize = self.currentSize - 1

能在O(n)的开销下能生成二叉堆看起来有点不可思议,其证明超出了本书的范围。但是,要理解用O(n)的开销能生成堆的关键是因为logn因子基于树的高度。而对于buildHeap里的许多操作,树的高度比logn要小。

Python 相关文章推荐
Python格式化css文件的方法
Mar 10 Python
Flask框架的学习指南之开发环境搭建
Nov 20 Python
python utc datetime转换为时间戳的方法
Jan 15 Python
使用coverage统计python web项目代码覆盖率的方法详解
Aug 05 Python
python多环境切换及pyenv使用过程详解
Sep 27 Python
将python2.7添加进64位系统的注册表方式
Nov 20 Python
使用Python给头像加上圣诞帽或圣诞老人小图标附源码
Dec 25 Python
tensorboard实现同时显示训练曲线和测试曲线
Jan 21 Python
Python Scrapy框架第一个入门程序示例
Feb 05 Python
在pycharm中创建django项目的示例代码
May 28 Python
python用字节处理文件实例讲解
Apr 13 Python
Python序列化模块JSON与Pickle
Jun 05 Python
Python实现二叉搜索树
Feb 03 #Python
Python的组合模式与责任链模式编程示例
Feb 02 #Python
举例讲解Python中的Null模式与桥接模式编程
Feb 02 #Python
简介Python设计模式中的代理模式与模板方法模式编程
Feb 02 #Python
Python找出9个连续的空闲端口
Feb 01 #Python
Python 爬虫的工具列表大全
Jan 31 #Python
python在不同层级目录import模块的方法
Jan 31 #Python
You might like
?繁体转换的class
2006/10/09 PHP
ThinkPHP查询返回简单字段数组的方法
2014/08/25 PHP
PHP生成随机字符串(3种方法)
2015/09/25 PHP
日常整理PHP中简单的图形处理(经典)
2015/10/26 PHP
[原创]smarty简单模板变量输出方法
2016/07/09 PHP
Javascript实现的分页函数
2007/02/07 Javascript
用javascript实现页面打印的三种方法
2007/03/05 Javascript
Javascript 实用小技巧
2010/04/07 Javascript
关于js中window.location.href,location.href,parent.location.href,top.location.href的用法与区别
2010/10/18 Javascript
JavaScript XML和string相互转化实现代码
2011/07/04 Javascript
jQuery插件实现控制网页元素动态居中显示
2015/03/24 Javascript
基于BootStrap Metronic开发框架经验小结【三】下拉列表Select2插件的使用
2016/05/12 Javascript
Bootstrap表单使用方法详解
2017/02/17 Javascript
微信小程序 定位到当前城市实现实例代码
2017/02/23 Javascript
解决ionic和angular上拉加载的问题
2017/08/03 Javascript
关于vue.js发布后路径引用的问题解决
2017/08/15 Javascript
支付宝小程序tabbar底部导航
2018/11/06 Javascript
简单分析js中的this的原理
2019/08/31 Javascript
[01:38]2018DOTA2亚洲邀请赛主赛事第二日现场采访 神秘商人痛陈生计不易
2018/04/05 DOTA
编程语言Python的发展史
2014/09/26 Python
简单的Python抓taobao图片爬虫
2014/10/26 Python
python PIL模块与随机生成中文验证码
2016/02/27 Python
python3.5+tesseract+adb实现西瓜视频或头脑王者辅助答题
2018/01/17 Python
python方向键控制上下左右代码
2018/01/20 Python
基于pandas数据样本行列选取的方法
2018/04/20 Python
对python:循环定义多个变量的实例详解
2019/01/20 Python
windows 10 设定计划任务自动执行 python 脚本的方法
2019/09/11 Python
Python PIL库图片灰化处理
2020/04/07 Python
Python如何合并多个字典或映射
2020/07/24 Python
CSS3实现跳动的动画效果
2016/09/12 HTML / CSS
简述数组与指针的区别
2014/01/02 面试题
编写一个类体现构造,公有,私有方法,静态,私有变量
2013/08/10 面试题
采购部部长岗位职责
2014/02/06 职场文书
校庆接待方案
2014/03/18 职场文书
CSS3实现的侧滑菜单
2021/04/27 HTML / CSS
PyTorch 实现L2正则化以及Dropout的操作
2021/05/27 Python