基于数组实现的大根堆——python
·
1. 什么是大根堆?
大根堆是一种特殊的完全二叉树,其核心特性是:每个父节点的值大于或等于其左右子节点的值。这意味着堆的根节点(堆顶)始终是整个堆中的最大值,且插入、删除元素后可通过 “堆化” 操作快速恢复这一特性。
完全二叉树的结构特点(除最后一层外每层节点全满,最后一层节点从左到右连续排列),使其非常适合用数组存储 —— 这也是数组实现大根堆的核心优势。
2. 为什么用数组实现大根堆?
数组实现大根堆的核心依据是完全二叉树的下标特性:若数组 heap 存储堆元素,对于下标为 i 的节点:
- 左子节点下标 =
2*i + 1 - 右子节点下标 =
2*i + 2 - 父节点下标 =
(i - 1) // 2
这种特性让我们无需额外存储指针(如树形节点的 left/right),仅通过下标计算就能快速定位父子节点,大幅降低了空间开销和操作复杂度。
3. 核心操作详解
基于数组的大根堆有两个核心操作:add(插入元素)和 pop(弹出堆顶元素),两者都依赖 “堆化” 过程维持大根堆性质。
(1)插入元素(add 方法)
插入元素的目标是:在维持完全二叉树结构的前提下,确保新元素插入后仍满足 “父≥子”。步骤:
- 尾插:将新元素添加到数组末尾(对应完全二叉树的最后一个叶子节点);
- 向上堆化:从新元素开始,逐层与父节点比较。若新元素值更大,则交换两者,直到父节点值更大或到达根节点。
示例:插入 3 到堆 [8,7,5,4,6,1] 中:
- 尾插后数组为
[8,7,5,4,6,1,3]; - 新元素
3的父节点是1(下标 2),3 > 1→ 交换得[8,7,5,4,6,3,1]; - 继续比较父节点
5(下标 2),3 < 5→ 停止堆化,最终堆为[8,7,5,4,6,3,1]。
(2)弹出堆顶元素(pop 方法)
弹出堆顶(最大值)的目标是:删除最大值后,仍维持完全二叉树结构和 “父≥子” 规则。步骤:
- 交换:将堆顶元素(下标 0)与数组末尾元素交换;
- 删除:移除数组末尾元素(原堆顶);
- 向下堆化:从新堆顶(原末尾元素)开始,逐层与左右子节点中较大者比较。若父节点值更小,则交换两者,直到父节点值更大或到达叶子节点。
示例:弹出堆 [8,7,5,4,6,3,1] 的堆顶 8:
- 交换堆顶与末尾元素 →
[1,7,5,4,6,3,8]; - 删除末尾元素 →
[1,7,5,4,6,3]; - 向下堆化:新堆顶
1与左右子节点7(左)、5(右)比较,1 < 7→ 交换得[7,1,5,4,6,3]; - 继续比较
1与左右子节点4、6,1 < 6→ 交换得[7,6,5,4,1,3],此时满足大根堆规则。
4. 数组实现的优势
- 空间高效:无需存储指针,仅用数组下标计算父子关系,空间复杂度
O(n)(n为元素数); - 操作高效:插入和弹出的堆化过程仅需遍历树高(
O(log n)),远快于数组的O(n)操作; - 实现简洁:核心逻辑依赖下标计算,代码量少,易于理解和维护。
5. 适用场景
基于数组的大根堆是实现优先队列的核心数据结构,广泛应用于:
- 任务调度(优先级高的任务先执行);
- 堆排序(利用堆顶是最大值的特性排序);
- Top K 问题(快速找到前 K 个最大值)。
6.代码
class BigHeap:
"""基于数组实现的大根堆
大根堆是满足"父节点值 ≥ 左右子节点值"的完全二叉树,本实现通过数组存储元素,
利用完全二叉树的下标特性快速定位父子节点,支持高效的插入和弹出操作。
"""
def __init__(self):
self.heap = [] # 用数组存储堆元素,下标对应完全二叉树的节点编号
self.size = 0 # 堆中元素的实际数量(避免频繁调用len(heap))
def __len__(self):
"""支持用len(heap)获取堆大小,符合Python容器规范"""
return self.size
def add(self, val):
"""向堆中插入元素,并通过向上堆化维持大根堆性质
步骤:
1. 将新元素添加到数组末尾(对应完全二叉树的最后一个叶子节点)
2. 向上堆化:从新元素开始,逐层与父节点比较,若子节点值更大则交换,
直到父节点值更大或到达根节点(保证父≥子)
"""
# 1. 新元素插入数组末尾
self.heap.append(val)
# 记录新元素的初始下标(当前堆大小即为新元素的索引)
current_idx = self.size
# 更新堆大小
self.size += 1
# 2. 向上堆化
while current_idx > 0: # 未到达根节点(根节点下标为0)
# 计算父节点下标:完全二叉树中,子节点i的父节点为 (i-1)//2
parent_idx = (current_idx - 1) // 2
# 若子节点值 > 父节点值,违反大根堆规则,交换两者
if self.heap[current_idx] > self.heap[parent_idx]:
self.heap[current_idx], self.heap[parent_idx] = self.heap[parent_idx], self.heap[current_idx]
# 继续向上比较父节点的父节点
current_idx = parent_idx
else:
# 父节点值更大,已满足大根堆规则,停止堆化
break
def pop(self):
"""弹出堆顶元素(最大值),并通过向下堆化维持大根堆性质
步骤:
1. 交换堆顶(下标0)与数组末尾元素(最后一个叶子节点)
2. 删除数组末尾元素(原堆顶,已被交换到末尾)
3. 向下堆化:从新堆顶开始,逐层与左右子节点中较大者比较,
若父节点值更小则交换,直到父节点值更大或到达叶子节点
"""
if self.size == 0:
raise Exception('empty heap!') # 边界处理:堆为空时无法弹出
# 1. 交换堆顶与末尾元素(保证删除后仍为完全二叉树)
self.heap[0], self.heap[-1] = self.heap[-1], self.heap[0]
# 2. 删除末尾元素(原堆顶,此时已被交换到最后)
self.heap.pop()
# 更新堆大小
self.size -= 1
# 3. 向下堆化:从新堆顶(原末尾元素)开始调整
current_idx = 0
while True:
# 计算左右子节点下标:左子节点 = 2*i + 1,右子节点 = 2*i + 2
left_idx = 2 * current_idx + 1
right_idx = 2 * current_idx + 2
# 情况1:当前节点无左子节点(已是叶子节点),无需继续调整
if left_idx >= self.size:
break
# 情况2:只有左子节点(无右子节点)
elif right_idx >= self.size:
# 若父节点值 < 左子节点值,交换并继续向下调整
if self.heap[current_idx] < self.heap[left_idx]:
self.heap[current_idx], self.heap[left_idx] = self.heap[left_idx], self.heap[current_idx]
current_idx = left_idx
else:
# 父节点值更大,满足规则,停止调整
break
# 情况3:同时有左右子节点
else:
# 若父节点值 ≥ 左右子节点,满足大根堆规则,停止调整
if self.heap[current_idx] >= self.heap[left_idx] and self.heap[current_idx] >= self.heap[right_idx]:
break
# 否则,与左右子节点中较大的一个交换(保证父节点始终是最大值)
elif self.heap[left_idx] > self.heap[right_idx]:
self.heap[current_idx], self.heap[left_idx] = self.heap[left_idx], self.heap[current_idx]
current_idx = left_idx # 继续调整左子树
else:
self.heap[current_idx], self.heap[right_idx] = self.heap[right_idx], self.heap[current_idx]
current_idx = right_idx # 继续调整右子树
# 测试代码
if __name__ == '__main__':
heap = BigHeap()
# 插入测试数据
l = [5, 4, 8, 7, 6, 1, 3]
for val in l:
heap.add(val)
print("插入后堆的结构:", heap.heap) # 输出:[8, 7, 5, 4, 6, 1, 3](大根堆,堆顶为8)
print("堆的大小:", len(heap)) # 输出:7
# 弹出堆顶(最大值8)
heap.pop()
print("弹出堆顶后堆的结构:", heap.heap) # 输出:[7, 6, 5, 4, 3, 1](新堆顶为7)
print("堆的大小:", len(heap)) # 输出:6
更多推荐


所有评论(0)