介绍
二叉堆是一种特殊的数据结构,类似于一棵完全二叉树,并且非叶子结点的值都大于或小于左右孩子结点所对应的值。二叉堆可以分为最大堆和最小堆,两者的区别在于非叶子结点的值是大于还是小于孩子结点的值,若大于,则为最大堆,若小于,则为最小堆。如图(a)就不是一个二叉堆,因为第2个结点的值为6,其孩子结点的值都大于它,而其它非叶子结点的值都大于孩子结点。图(b)为最大堆,图©为最小堆。
![]() | ![]() | ![]() |
---|---|---|
图(a) | 图(b) | 图© |
使用二叉堆数据结构,可以实现堆排序,时间复杂度为O(nlgn),并且可以实现就地排序,不需要开辟太多额外的空间,可以算是非常优秀的排序算法。除了能够应用于排序外,还可以构建优先队列,利用最大堆构建,能够优先输出等级最高的结点,例如共享计算机系统的作业调度,优先队列记录将要执行的各个作业以及它们之间的相对优先级,当一个作业被完成或者中断时,调度器选择下一个优先级最高的作业去执行。
应用一:堆排序
在实现堆(最大堆)排序之前,需要完成两个关键过程:
- 维持最大堆:维持最大堆的性质
- 创建最大堆:从一个无序的数组构建最大堆
维持最大堆
给定一个数组和结点索引,在该结点的左右子树为最大堆的前提下,要保证以该结点为根节点的树依然是个最大堆。可以简单的理解为随意更改某个结点的值后,破坏了原来最大堆的性质,因为可能修改的值小于左右孩子结点的值,所以需要通过一个方法维持最大堆的性质,在调用该方法后,以该结点为根节点的树依然要是个最大堆。例如图(a),第二个结点的值为6,小于左右孩子结点的值10和9,当调用该方法后,6和10两值交换,保证了以第二个结点为根节点的树为最大堆。时间复杂度为O(lgn)。
def Max_Heapify(arr:List[int],i:int,heap_size:int):
# 双亲结点的索引
def Parent(i:int):
return int(i>>1)
# 左孩子的索引
def Left(i:int):
return int(i<<1)
# 右孩子的索引
def Right(i:int):
return int(i<<1)+1
while True:
l,r = Left(i),Right(i)
if l<=heap_size and arr[l-1] > arr[i-1]:
larger = l
else:
larger = i
if r<=heap_size and arr[r-1] > arr[larger-1]:
larger = r
if larger != i:
# 交换根结点与最大孩子结点的值
arr[i-1],arr[larger-1] = arr[larger-1],arr[i-1]
i = larger
else:
break
已知一个结点索引,需要获得该结点的双亲结点、左右孩子结点的索引,由于二叉堆为一个完全二叉树,根据其性质,很容易获得。这里并没有采用递归调用的方式,而是采用一个while循环实现值的交换,据算法导论一书所说,递归调用可能会使某些编译器产生低效的代码。
创建最大堆
在创建最大堆之前,需要知道一个定理:当用数组存储一个最大堆时,叶子结点所对应的索引为[n/2]+1,[n/2]+2,……,n。由于叶子结点没有左右孩子结点,所以以叶子结点为根节点的树为一个最大堆。通过该定理,我们可以自底向上的创建一个最大堆,以[n/2],[n/2]-1,……,2的顺序来维持最大堆,保证了左右子树为最大堆的前提,最终完成最大堆的创建。时间复杂度为O(n)。
def BuildMaxHeap(arr:List[int],heap_size:int):
for i in range(1,int(heap_size//2))[::-1]:
Max_Heapify(arr,i,heap_size)
堆排序
前面两个过程实现后,堆排序就很容易实现了。首先从一个无序数组创建一个最大堆,由于最大堆的根节点的值是堆中所有结点值中最大的,所以我们每次将根节点的值与最后一个结点的值交换,并删除最后一个结点(可以通过减少heap_size的值来实现对结点的删除),由于根节点的值被修改,为了保持最大堆,调用维持最大堆方法。时间复杂度为O(nlgn)。
def HeapSort(arr:List[int]):
heap_size = len(arr)
# 同一数组初始顺序不同,构建的最大堆也不一样,但不影响最终的排序结果
BuildMaxHeap(arr,heap_size)
for i in range(1,len(arr))[::-1]:
# exchange arr[i] with arr[1]
arr[0],arr[i] = arr[i],arr[0]
heap_size -= 1
Max_Heapify(arr,1,heap_size)
应用二:优先队列
优先队列具有四个基本方法,以最大堆为例,
- Insert(value):在队列中插入一个值,时间复杂度为O(lgn)。
- Maximum():返回队列中最大的值,时间复杂度为O(1)。
- Extract_Max():去掉并返回队列中最大的值,时间复杂度为O(lgn)。
- Increase_Key(i,key):将堆中其中一个元素的关键值增加到key,其中key要大于原键值,时间复杂度为O(lgn)。
第二个方法很容易实现,直接返回根节点的值。
第三个方法不仅要返回值,还要将其在队列中删除。这里先将堆中最后的结点值与根节点值交换,然后删除最后的结点,最后调用维持最大堆的方法。与堆排序方法中循环体内部的过程类似。
第四个方法由于增大了某结点的值,不影响以该结点为根节点的树为最大堆的性质,但是会影响双亲结点。所以需要不断地比较修改的结点与双亲结点的值,若不满足最大堆条件,则交换两个结点的值
第一个方法,首先增加一个值为-inf的叶子结点,然后对该叶子结点调用第四个方法即可。
class PriorityQueue(object):
"""优先队列具有四个基本方法:
Insert:把元素插入到堆中
Maximum:返回堆中的最大键值的元素
Extract_Max:删除堆中的最大键值的元素并返回该元素
Increase_Key:将堆中其中一个元素的关键值增加到k,其中k要大于原键值
"""
def __init__(self,arr:List[int]):
self.heap_size = len(arr)
self.arr = arr
BuildMaxHeap(self.arr,self.heap_size)
def Is_empty(self):
return self.heap_size == 0
def Maximum(self):
if self.Is_empty():
raise IndexError("the queue is empty! Please insert a new element")
return self.arr[0]
def Extract_Max(self):
if self.Is_empty():
raise IndexError("the queue is empty! Please insert a new element")
# 存储堆中最大键值
max = self.arr[0]
# 用堆中最后一个叶子结点的关键值替换根节点的关键值
self.arr[0] = self.arr[self.heap_size-1]
# 堆元素数量减一
del self.arr[self.heap_size-1]
self.heap_size -= 1
# 维持最大堆
MAX_Heapify2(self.arr,1,self.heap_size)
return max
def Increase_Key(self,i:int,key:int):
"""
:param i: 要操作元素的索引
:param key: 关键值要增加到值
"""
if self.Is_empty():
raise IndexError("the queue is empty! Please insert a new element")
if i > self.heap_size:
raise IndexError("the index over the max number of element")
if key<=self.arr[i-1]:
raise ValueError("key must greater than value")
self.arr[i-1] = key
# 为了维持最大堆,需要不断地比较修改的结点与双亲结点的关键值,若不满足最大堆条件,则交换两个结点的关键值
while i>1 and self.arr[int(i>>1)-1] < self.arr[i-1]:
self.arr[i-1],self.arr[int(i>>1)-1] = self.arr[int(i>>1)-1],self.arr[i-1]
i = int(i>>1)
def Insert(self,value:int):
self.arr.append(float("-inf"))
self.heap_size += 1
self.Increase_Key(self.heap_size,value)
if __name__ == '__main__':
arr = [1,5,7,12,4,8,9,0,27,17,3,16,13,10]
P_queue = PriorityQueue(arr)
print(P_queue.Maximum())
print(P_queue.Extract_Max())
P_queue.Increase_Key(6,30)
P_queue.Insert(14)
print(P_queue.Maximum())
部分内容引用于算法导论一书