1 引言
堆(Heap)本质上是一种基于数组实现的完全二叉树。它通过严格的父子节点大小约束,保证能在 O(1) 的时间内获取极值。在底层原理与系统设计中,堆既是高效的优先队列,也是操作系统调度的核心基石。
2 基本概念
2.1 数据机构
从数据结构上看,它本质上就是一个数组。```c
1, 3, 2, 7, 5, 8, 4
模拟的树结构:
```c
1
/ \
3 2
/ \ / \
7 5 8 4
2.2 结构性
它必须是一棵完全二叉树:除最后一层外,其余各层节点全部填满,且最后一层的节点靠左排列。
大顶堆 :任意节点的值 ≥ 其子节点的值(根节点最大)。
小顶堆:任意节点的值 ≤ 其子节点的值(根节点最小)。
正因为是完全二叉树,每个节点都可以直接映射到数组下标。假设当前节点的数组下标为 i,则:
- 其父节点下标:
(i - 1) / 2 - 其左子节点下标:
2 * i + 1 - 其右子节点下标:
2 * i + 2## 3 操作
3 建堆
3.1 步骤
大顶堆:
以大顶堆为例:
- 建立数组
arr[N],用于存储原始数据。 - 找到最后一个叶子节点,即
arr[N-1]。 - 找到从后往前的第一个非叶子节点,即最后一个节点的父节点:
((N-1) - 1) / 2 = m。 - 从下标
m开始,对每个非叶子节点执行下沉操作:
(1)将父节点m与其子节点2*m + 1、2*m + 2比较,若父节点小于子节点的最大者,则交换两者位置;
(2)若在(1)中m与2*m + 1发生了交换,则继续对2*m + 1执行步骤(1)。 - 依次对下标
m-1, m-2, ... 0按步骤 4 处理。
3.2 举例
c
4
/ \
10 3
/ \ / \
5 1 8 7
-
建数组
[4, 10, 3, 5, 1, 8, 7] -
找到最后一个叶子节点,最后一个叶子节点为
7,对应下标为6 -
找到第一个非叶子节点,计算公式:
(6-1)/2 = 2 -
下标
2数值是3,其子节点下标为5,6,数值分别是8,7(1)
3 < 8,交换两者位置:[4, 10, 8, 5, 1, 3, 7],3无子节点不用继续计算 -
下标
1数值是10,其子节点下标为3,4,数值分别是5,1(1)
5 < 10,无需交换 -
下标
0数值是4,其子节点下标为1,2,数值分别是10,8(1)
4 < 10,交换两者位置:[10, 4, 8, 5, 1, 3, 7](2)发生了交换,继续对
4做下沉。(3)
4 < 5,交换两者位置:[10, 5, 8, 4, 1, 3, 7](4)发生了交换,继续对
4做下沉,4是叶子节点无需再进行
大顶堆建好:
c
10
/ \
5 8
/ \ / \
4 1 3 7
4 堆排序
堆排序就是把数组原地看成一个大顶堆,反复把堆顶最大值换到数组末尾,从而完成排序。
大顶堆为例:
- 建堆
- 交换堆顶和末尾:将当前最大数据放到后面
- 对剩余元素重新建堆
- 重复2和3步骤
5 插入和删除
5.1 步骤
- 插入(上浮):将新元素放到数组末尾,然后不断与父节点比较。如果违背了堆序性(比如小顶堆中比父节点小),就交换位置,直到满足条件。
- 删除堆顶(下沉):将数组末尾元素移到堆顶,然后不断与较小的子节点(小顶堆)比较并交换,直到沉到合适的位置。
6 时间复杂度
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 查询堆顶 | O(1) | 直接取根节点,最大/最小值 |
| 插入 | O(log n) | 放到末尾后上浮 |
| 删除堆顶 | O(log n) | 末尾元素换到堆顶后下沉 |
| 建堆 | O(n) | 自底向下调整建堆 |
| 堆排序 | O(n log n) | 建堆 O(n) + n 次删除堆顶,每次 O(log n) |
关于建堆的时间复杂度:
完全二叉树里,越底层的节点越多,但它们能下沉的距离越短,且非叶子节点只有n/2个,
最终计算下来时间复杂度能收敛在O(n)级别。