python实现堆和索引堆的代码示例


Posted in Python onMarch 19, 2018

堆是一棵完全二叉树。堆分为大根堆和小根堆,大根堆是父节点大于左右子节点,并且左右子树也满足该性质的完全二叉树。小根堆相反。可以利用堆来实现优先队列。

由于是完全二叉树,所以可以使用数组来表示堆,索引从0开始[0:length-1]。结点i的左右子节点分别为2i+1,2i+2。长度为length的树的最后一个非叶子节点为length//2-1。当前节点i的父节点为(i-1)//2。其中//表示向下取整。

以大根堆举例。当每次插入或者删除的时候,为了保证堆的结构特征不被破坏,需要进行调整。调整分为两种,一种是从上往下,将小的数下沉。一种是从下往上,令大的数上浮。

具体实现如下:

首先编写几个魔术方法。包括构造函数,可以直接调用len来返回data数组长度的函数,一个打印data内容的函数

def __init__(self, data=[]):
    self.data = data
    self.construct_heap()

 def __len__(self):
    return len(self.data)

 def __str__(self):
    return str(self.data)

定义一个swap函数,来方便的交换数组中两个索引处的值。

def swap(self, i, j):
    self.data[i], self.data[j] = self.data[j], self.data[i]

定义float_up方法,使堆中大的数能浮上来。当前节点不为根节点,并且当前节点数据大小大于父节点时,上浮。

def float_up(self, i):
    while i > 0 and self.data[i] > self.data[(i - 1) // 2]:
      self.swap(i, (i - 1) // 2)
      i = (i - 1) // 2

定义sink_down方法,使堆中小的数沉下去。当前节点不为叶子节点时,如果小于左孩子或右孩子的数据,则和左右孩子中较大的换一下位置。

def sink_down(self, i):
    while i < len(self) // 2:
      l, r = 2 * i + 1, 2 * i + 2
      if r < len(self) and self.data[l] < self.data[r]:
        l = r
      if self.data[i] < self.data[l]:
        self.swap(i, l)

      i = l

实现append方法,能够动态地添加数据。在数据数组尾部添加数据,然后将数据上浮。

def append(self, data):
    self.data.append(data)
    self.float_up(len(self) - 1)

实现pop_left方法,取堆中最大元素,即优先队列中第一个元素。将数组中第一个元素与最后一个元素换位置,删除最后一个元素,然后将第一个元素下沉到合适的位置。

def pop_left(self):
    self.swap(0, len(self) - 1)
    r = self.data.pop()
    self.sink_down(0)
    return r

如果想在初始化堆的时候,向构造函数中传入数据参数,则需要一次性将整个堆构建完毕,而不能一个一个加入。实现也很简单,从最后一个非叶节点开始,逐个执行sink_down操作。

def construct_heap(self):
    for i in range(len(self) // 2 - 1, -1, -1):
      self.sink_down(i)

这样一个基本的堆的代码就编写完毕了。

但是如果我们想要动态的改变数据,当前的堆就不能满足我们的需求了,因为索引不能总是标识同一个数据,因为堆的结构是不断调整的。我们需要使用索引堆。

在索引堆中,我们不在堆中直接保存数据,而是用在堆中存放数据的索引。

如果我们输入的数据arr是 45 20 12 5 35。则arr[0]一直指向45,arr[1]一直指向20,因为我们在调整堆结构中实际调整的是索引数组,而不会改变真实存放数据的数组。

因此我们的代码需要调整,首先在构造函数中加入一个索引数组。下标从0开始,与存放数据的数组的下标相对应。

def __init__(self, data=[]):
    self.data = data
    self.index_arr = list(range(len(self.data)))
    self.construct_heap()

然后将返回堆长度的魔术函数也修改一下。

def __len__(self):
    return len(self.index_arr)

调整一下之前定义的swap方法,原来是直接交换数据,现在交换索引。

def swap(self, i, j):
    self.index_arr[i], self.index_arr[j] = self.index_arr[j], self.index_arr[i]

调整float_up以及sink_down中的相应位置

def float_up(self, i):
    while i > 0 and self.data[self.index_arr[i]] > self.data[self.index_arr[(i - 1) // 2]]:
      self.swap(i, (i - 1) // 2)
      i = (i - 1) // 2
      
  def sink_down(self, i):
    while i < len(self) // 2:
      l, r = 2 * i + 1, 2 * i + 2
      if r < len(self) and self.data[self.index_arr[l]] < self.data[self.index_arr[r]]:
        l = r
      if self.data[self.index_arr[i]] < self.data[self.index_arr[l]]:
        self.swap(i, l)

      i = l

当append数据的时候,要相应的更新index_arr

def append(self, data):
    self.data.append(data)
    self.index_arr.append(len(self))
    self.float_up(len(self) - 1)

当移出数据的时候,之前已经提到过存放数据的数组,是按照append的顺序进行存储的,平时操作只是对index_arr的顺序进行调整。

如果data_arr为 42 30 74 60 相应的index_arr应该为2 3 0 1

这时,当我们popleft出最大元素时,data_arr中的74被移出后变成了42 30 60,数组中最大索引由3变成了2,如果索引数组中仍然用3这个索引来索引30会造成index溢出。74的索引为2,需要我们将索引数在2之后的都减1。

综上,在删除元素时,我们原先是将data_arr中的首尾元素互换,再删除尾部元素,再对头部元素进行sink_down操作。现在我们先换索引数组中首尾元素,再删除索引数组尾部元素,此时尚未操作存放data的data_arr,因此索引数组剩余元素与data_arr的元素仍是一一对应的。进行sink_down操作,操作完成之后再删除data_arr相应位置元素。最后将index_arr中值大于原index_arr头部元素值的减一。

def pop_left(self):
    self.swap(0, len(self) - 1)
    r = self.index_arr.pop()
    self.sink_down(0)
    self.data.pop(r)

    for i, index in enumerate(self.index_arr):
      if index > r:
        self.index_arr[i] -= 1

    return r

索引堆增加了一个更新操作,可以随时更新索引堆中的数据。更新时,先直接更新data_arr中相应索引处的数据,然后在index_arr中,找到存放了data_arr中,刚被更新的数据的索引的索引位置,与删除时一样需要进行一次遍历。找到这个位置之后,由于无法确定与前后元素的大小关系,因此需要进行一次float_up操作再进行一次sink_down操作。

def update(self, i, data):
    self.data[i] = data
    for index_index, index in enumerate(self.index_arr):
      if index == i:
        target = index_index

    self.float_up(target)
    self.sink_down(target)

可以很明显看出,这个索引堆在插入元素时是比较快的,但是在删除元素和更新元素时,为了查找相应位置索引,都进行了一次遍历,这是很耗时的操作。为了能更快的找到index_arr中值为要更新的data_arr的相应索引值得索引位置,我们再次开辟一个新的数组to_index,来对index_arr进行索引。

例如对于数组75 54 65 90

此时它的index_arr为3 0 2 1。当要更新data[3],即90这个元素时,现在要遍历一遍index_arr来找到3这个位置,这个位置是0。我们要建立一个to_index,to_index[3]中存放的元素为0。

index_arr存放的元素分别为: 1 3 2 0。

先改变swap数组,在交换index_arr中元素时,也交换存放在to_index中的index_arr的索引。

def swap(self, i, j):
    self.index_arr[i], self.index_arr[j] = self.index_arr[j], self.index_arr[i]
    self.to_index[self.index_arr[i]], self.to_index[self.index_arr[j]] = self.to_index[self.index_arr[j]], \
                                       self.to_index[self.index_arr[i]]

然后在update中,当要更新位置为i的元素时,我们就不需要通过一次遍历才能找到index_arr中该元素的索引,而是直接通过访问index_arr[i]即可访问index_arr中相应索引

def update(self, i, data):
    self.data[i] = data
    target = self.to_index[i]
    self.float_up(target)
    self.sink_down(target)

最后改变pop_left中相应代码,这时我们需要维护三个数组,data_arr,index_arr以及to_index。

仍然是首先将index_arr首位元素交换,并pop出尾部元素存放到i中。然后将头部元素sink_down到相应位置,然后将pop出data_arr索引i处的元素。然后pop出to_index中索引为i的元素,再将index_arr中索引溢出的元素进行调整。

def pop_left(self):
    self.swap(0, len(self) - 1)
    r = self.index_arr.pop()

    self.sink_down(0)
    self.data.pop(r)

    self.to_index.pop(r)
    for i in range(r, len(self)):
      self.index_arr[self.to_index[i]] -= 1

    return r

以上就是python实现对和索引堆的具体方式。希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python+django实现简单的文件上传
Aug 17 Python
python3利用Dlib19.7实现人脸68个特征点标定
Feb 26 Python
可能是最全面的 Python 字符串拼接总结【收藏】
Jul 09 Python
Flask框架各种常见装饰器示例
Jul 17 Python
python url 参数修改方法
Dec 26 Python
python DataFrame 取差集实例
Jan 30 Python
Python 实现输入任意多个数,并计算其平均值的例子
Jul 16 Python
如何基于Python批量下载音乐
Nov 11 Python
python将字符串转变成dict格式的实现
Nov 18 Python
解决pyCharm中 module 调用失败的问题
Feb 12 Python
python 解压、复制、删除 文件的实例代码
Feb 26 Python
利用Python制作动态排名图的实现代码
Apr 09 Python
python实现一个简单的并查集的示例代码
Mar 19 #Python
python使用筛选法计算小于给定数字的所有素数
Mar 19 #Python
python将每个单词按空格分开并保存到文件中
Mar 19 #Python
python将文本分每两行一组并保存到文件
Mar 19 #Python
python: line=f.readlines()消除line中\n的方法
Mar 19 #Python
Python File readlines() 使用方法
Mar 19 #Python
Python cookbook(数据结构与算法)筛选及提取序列中元素的方法
Mar 19 #Python
You might like
PHP4 与 MySQL 交互使用
2006/10/09 PHP
PHP中将数组转成XML格式的实现代码
2011/08/08 PHP
PHP实现用户异地登录提醒功能的方法【基于thinkPHP框架】
2018/03/15 PHP
tp5.1 框架join方法用法实例分析
2020/05/26 PHP
几行代码轻松搞定jquery实现flash8类似的连接效果
2007/05/03 Javascript
JQuery 前台切换网站的样式实现
2009/06/22 Javascript
浏览器脚本兼容 文本框中,回车键触发事件的兼容
2010/06/21 Javascript
JQuery each()函数如何优化循环DOM结构的性能
2012/12/10 Javascript
js事件冒泡实例分享(已测试)
2013/04/23 Javascript
jQuery让控件左右移动的三种实现方法
2013/09/08 Javascript
简介JavaScript中valueOf()方法的使用
2015/06/05 Javascript
JS延时提示框实现方法详解
2015/11/26 Javascript
基于jquery实现的仿优酷图片轮播特效代码
2016/01/13 Javascript
js将滚动条滚动到指定位置的简单实现方法
2016/06/25 Javascript
Jquery Easyui菜单组件Menu使用详解(15)
2016/12/18 Javascript
JS实现根据密码长度显示安全条功能
2017/03/08 Javascript
基于webpack 实用配置方法总结
2017/09/28 Javascript
深入浅析Node环境和浏览器的区别
2018/08/14 Javascript
微信小程序实现Session功能及无法获取session问题的解决方法
2019/05/07 Javascript
解决layUI的页面显示不全的问题
2019/09/20 Javascript
uni-app微信小程序登录授权的实现
2020/05/22 Javascript
python批量下载图片的三种方法
2013/04/22 Python
Python selenium 三种等待方式解读
2016/09/15 Python
Python 数据结构之堆栈实例代码
2017/01/22 Python
pandas修改DataFrame列名的方法
2018/04/08 Python
python tornado微信开发入门代码
2018/08/24 Python
对pandas写入读取h5文件的方法详解
2018/12/28 Python
拿来就用!Python批量合并PDF的示例代码
2020/08/10 Python
如何Tkinter模块编写Python图形界面
2020/10/14 Python
Django执行源生mysql语句实现过程解析
2020/11/12 Python
英国布鲁姆精品店:Bloom Boutique
2018/03/01 全球购物
北美最大的零售退货翻新商:VIP Outlet
2019/11/21 全球购物
药店营业员岗位职责
2015/04/14 职场文书
如何写新闻稿
2015/07/18 职场文书
家庭教育教师培训学习体会
2016/01/14 职场文书
创业计划书之餐饮馄饨店
2019/07/18 职场文书