数据结构——最大最小堆
堆介绍
堆(Heap)是树结构的一种,它是一颗完全二叉树,一个最小堆或最大堆总会满足下列条件:
- 堆中某个节点的值总是不大于或不小于其父节点的值
- 堆总是一棵完全二叉树
一个最大堆如下:
最大值在根节点,任意节点值总是大于该子树的最大值,而最小堆则相反
说了这么多关于堆定义的东西,我们发现堆本质还是上一章讲到的二叉树,堆能从二叉树中单独拎出来自然是有不少应用空间的
最常见的,优先队列,就可以通过最大堆结构来实现,再比如可以用最小堆来处理一个top k问题
实现思路
上面介绍过,堆是一颗完全二叉树,假如我们从根节点开始给每一个节点编号如下:
通过数学归纳,对于任意节点编号k:
- 父节点编号 = (k-1) // 2
- 左子节点编号 = k * 2 + 1
- 右子节点编号 = (k+1) * 2
既然我们可以通过任意编号k,找到它的父子节点编号,这意味着可以用一个数组来存储节点,这也是堆和其他树结构底层实现较大的不同
通常来讲,一个链表或者一颗树,不同节点是依赖节点之间的互相引用来保证数据结构的完整性,但对于堆来说我们只要通过索引来访问父子节点就可以了(实际上对于任意一颗完全二叉树来说都可以使用数组来保证数据完整,但缺乏应用意义)
使用数组作存储结构可以保证它是一颗完全二叉树,增删数据则需要保证堆中某个节点的值总是不小于其父节点的值(这里以最大堆为例)
通常一个最大堆,不会设计到查询数据和修改数据(实际上通过索引就很容易查询和修改任意节点数据)
对于增加数据,可以把新数据插入到数组末尾并拿到索引,通过索引找到父节点,如果大于父节点的值则交换位置,重复上述操作,直到父节点大于新数据停止。
对于删除数据,删除索引为k的节点,我们可以把数组最后一个元素移到索引k位置,然后让它和左右子节点中的最大值比较,如果小于则交换位置,重复上述操作即可。
具体代码
定义Maxheap类和基础功能
class Maxheap:
def __init__(self):
self._data = [] # 存储数据数组
def _parentId(self, index: int) -> int:
# 输入子节点的索引返回父节点索引
return (index - 1) // 2 if index > 0 else 0
def _leftId(self, index: int) -> int:
# 输入父节点的索引返回左子节点索引
return 2 * index + 1
def _rightId(self, index: int) -> int:
# 输入父节点的索引返回右子节点索引
return 2 * index + 2
@staticmethod
def _cmp_(m, o):
# 比较函数 修改_cmp_可实现最小堆
return m > o
解释一下_cmp_()方法,这个方法是用来给用户自定义如何比较的,假如一个堆存储的都是数字,那么比较运算符就可以解决问题,但如果存储的是用户定义的类实例,并且类中又没有实现python内置的比较方法,这时候用户就可以改变_comp_方法来自定义比较大小
给最大堆新增一个元素
def add(self, item):
# 向添加一个元素
# 思路:首先添加在数据最末尾 然后比较它的父节点 如果大于父节点 则交换两个节点位置 继续比较
self._data.append(item)
index = len(self._data) - 1
while self._cmp_(item, self._data[self._parentId(index)]):
# 交换位置
self._data[index], self._data[self._parentId(index)] = self._data[self._parentId(index)], self._data[index]
index = self._parentId(index)
取出最大值
def get(self):
# 取出堆中的最大值 也就是数组序号为0的值
# 后续处理思路:把数组最后一个元素插入到第一个元素,比较它和子节点的值,如果小于,则交换
if not self._data:
raise BlockingIOError
res = self._data[0]
if len(self._data) == 1:
self._data = []
return res
self._data[0] = self._data.pop()
index = 0
# 先判断有没有子节点
while True:
rightId = self._rightId(index)
leftId = self._leftId(index)
if len(self._data)-1 >= leftId:
# 找出左右节点最大值
if len(self._data)-1 == leftId or self._cmp_(self._data[leftId], self._data[rightId]):
swapId = leftId
else:
swapId = rightId
if self._cmp_(self._data[swapId], self._data[index]):
self._data[index], self._data[swapId] = self._data[swapId], self._data[index]
index = swapId
else:
break
else:
# 没有子节点 不用比较了 退出循环
break
return res
上面这几段代码整合起来,一个最大堆就实现了,虽然我没有实现删除任意节点的功能,但实现和get方法是一样的,有兴趣的朋友可以自己试着写一写remove(k)方法
扩展与其他
从原理和代码量来讲,相信堆很好理解,前面几篇数据结构中我们提到过队列,而堆可以用来实现优先队列,欢迎大家自由尝试