堆是一种图的树形结构,被用于实现“优先队列”(priority queues)
注:优先队列是一种数据结构,可以自由添加数据,但取出数据时要从最小值开始按顺序取出。在堆。在堆的树形结构中,各个顶点被称为“结点”(node),数据就存储在这些结点中。
图解:
1.结点内的数字就是存储的数据。堆中的每个结点最多有两个子结点。树的形状取决于数据的个数。另外,结点的排列顺序为从上到下,同一行里则为从左到右。
2.在堆中存储数据时必须遵守这样一条规则:子结点必定大于父结点。因此,最小值被存储在顶端的根结点中。往堆中添加数据时,为了遵守这条规则,一般会把新数据放在最下面一行靠左的位置。当最下面一行里没有多余空间时,就再往下另起一行,把数据加在这一行的最左端。
3.我们试试往堆里添加数字5。按照02的说明寻找新数据的位置。该图中最下面一排空着一个位置,所以将数据加在此处。
4.如果父结点大于子结点,则不符合上文提到的规则,因此需要交换父子结点的位置。这里由于父结点的6大于子结点的5,所以交换了这两个数字。重复这样的操作直到数据都符合规则,不再需要交换为止。
5.现在,父结点的1小于子结点的5,父结点的数字更小,所以不再交换。
这样,往堆中添加数据的操作就完成了。
6.从堆中取出数据时,取出的是最上面的数据。这样,堆中就能始终保持最上面的数据最小。
7.由于最上面的数据被取出,因此堆的结构也需要重新调整。按照01中说明的排列顺序,将最后的数据(此处为6)移动到最顶端。
8.如果子结点的数字小于父结点的,就将父结点与其左右两个子结点中较小的一个进行交换。
9.这里由于父结点的6大于子结点(右)的5大于子结点(左)的3,所以将左边的子结点与父结点进行交换。
现在,子结点(右)的8大于父结点的6大于子结点(左)的4,需要将左边的子结点与父结点进行交换。
10.重复这个操作直到数据都符合规则,不再需要交换为止。
这样,从堆中取出数据的操作便完成了。
解说
堆中最顶端的数据始终最小,所以无论数据量有多少,取出最小值的时间复杂度都为O(1)。
另外,因为取出数据后需要将最后的数据移到最顶端,然后一边比较它与子结点数据的大小,一边往下移动,所以取出数据需要的运行时间和树的高度成正比。假设数据量为n,根据堆的形状特点可知树的高度为log2n,那么重构树的时间复杂度便为O(logn)。
添加数据也一样。在堆的最后添加数据后,数据会一边比较它与父结点数据的大小,一边往上移动,直到满足堆的条件为止,所以添加数据需要的运行时间与树的高度成正比,也是O(logn)。
我们在此使用手动实现和Python中内置的模块heapq两种方式来实现堆
1.手动实现堆数据结构。在Python中,可以通过列表来实现堆,其中父节点和子节点之间的关系可以通过列表的索引来表示。
- 索引为
i
的节点:- 父节点的索引为
(i-1)//2
- 左子节点的索引为
2*i+1
- 右子节点的索引为
2*i+2
- 父节点的索引为
通过这种方式,可以在列表中轻松地找到任意节点的父节点和子节点。
注: 在算法实现中我们相比物理地址,更关心的其实是逻辑地址 (这里说的物理地址是列表,因为我们堆是用列表实现的, 下述的所有算法操作实际上都是列表操作)
因此实际上我们可以在纸上画一个从1到9的堆结构,并添加好索引index, 从堆的角度(逻辑地址的角度)来看代码,更方便理解算法
class MinHeap:
def __init__(self):
self.heap = [] # 初始化一个空列表用于存储堆元素
def heapify_up(self, index):
# 向上调整堆,保持最小堆性质 while True表示向上递归调整直到遇到break(当前节点大于父节点)或index<=0(到根节点)
while index > 0:
parent_index = (index - 1) // 2 # 计算父节点索引
if self.heap[index] < self.heap[parent_index]: # 如果当前节点小于父节点,交换位置
self.heap[index], self.heap[parent_index] = self.heap[parent_index], self.heap[index]
index = parent_index
else:
break
def heapify_down(self, index):
# 向下调整堆,保持最小堆性质 while True表示递归调整直到遇到break(当前节点无子节点或者当前节点的值大于左右子节点的值)
while True:
left_child_index = 2 * index + 1 # 计算左子节点索引
right_child_index = 2 * index + 2 # 计算右子节点索引
min_index = index
# 找到当前节点与其子节点中的最小值
if left_child_index < len(self.heap) and self.heap[left_child_index] < self.heap[min_index]:
min_index = left_child_index
if right_child_index < len(self.heap) and self.heap[right_child_index] < self.heap[min_index]:
min_index = right_child_index
if min_index != index:
# 如果需要交换节点位置,则进行交换并更新当前节点索引
self.heap[index], self.heap[min_index] = self.heap[min_index], self.heap[index]
index = min_index
else:
break
def insert(self, value):
# 插入新元素并调整堆,保持最小堆性质
self.heap.append(value)
self.heapify_up(len(self.heap) - 1)
def extract_min(self):
# 提取堆顶元素(最小值)并调整堆,保持最小堆性质
if not self.heap:
return None
min_value = self.heap[0]
self.heap[0] = self.heap[-1] # 将最后一个元素移动到根节点
self.heap.pop() # 弹出最后一个元素
self.heapify_down(0)
return min_value
def get_min(self):
# 获取堆顶元素(最小值),但不删除
if not self.heap:
return None
return self.heap[0]
def size(self):
# 获取堆的大小(元素个数)
return len(self.heap)
@classmethod
def heapify(cls, lst):
# 类方法:从已有列表建立最小堆
heap = cls() # 创建一个新的最小堆实例
heap.heap = lst # 将传入的列表作为堆的底层存储
# 从最后一个非叶子节点开始到根节点,依次进行向下调整,保持最小堆性质
for i in range(len(heap.heap)//2, -1, -1):
heap.heapify_down(i)
return heap
# 使用自定义的最小堆数据结构
min_heap = MinHeap()
min_heap.insert(3)
min_heap.insert(1)
min_heap.insert(2)
min_heap.insert(4)
min_heap.insert(5)
print(min_heap.size()) # 输出:5
print(min_heap.extract_min()) # 输出:1
print(min_heap.extract_min()) # 输出:2
print(min_heap.extract_min()) # 输出:3
print(min_heap.extract_min()) # 输出:4
# 从已有列表建立最小堆
lst = [7, 4, 5, 1, 2, 8, 3, 9]
new_heap = MinHeap.heapify(lst)
print(new_heap.size())# 输出:8
print(new_heap.extract_min())# 输出:1
print(new_heap.extract_min())# 输出:2
print(new_heap.size())# 输出:6
print(new_heap.get_min()) # 输出:3
print(new_heap.size())# 输出:6
2.内置的模块heapq实现
import heapq
# 将列表转换为堆
lst = [3, 1, 2]
heapq.heapify(lst) # 转换为最小堆
# 插入元素到堆中
heapq.heappush(lst, 4)
# 弹出堆顶元素(最小值)
min_value = heapq.heappop(lst)
print(min_value) # 输出:1
print(lst) # 输出:[2, 4, 3]
文章来源:书籍《我的第一本算法书》
书籍链接:
我的第一本算法书 (豆瓣) (douban.com)
作者:宫崎修一 石田保辉
出版社:人民邮电出版社
ISBN:9787115495242
本篇文章仅用于学习和研究目的,不会用于任何商业用途。引用书籍《我的第一本算法书》的内容旨在分享知识和启发思考,尊重原著作者宫崎修一和石田保辉的知识产权。如有侵权或者版权纠纷,请及时联系作者。