优先队列(堆)
一、ADT模型与核心操作
1. 抽象数据类型定义
对象:包含零个或多个元素的集合,每个元素有优先级标识。
核心操作:
操作 | 功能说明 | 时间复杂度目标 |
---|---|---|
Initialize |
初始化空队列 | O(1) |
Insert |
插入元素并维护优先级结构 | O(log N) |
DeleteMin /DeleteMax |
删除并返回最高优先级元素 | O(log N) |
FindMin /FindMax |
查询最高优先级元素(不删除) | O(1) |
二、简单实现方案对比
1. 不同数据结构的时间复杂度
实现方式 | 插入 | 删除极值 | 适用场景 |
---|---|---|---|
无序数组 | O(1) | O(N) | 删除操作极少 |
有序数组 | O(N) | O(1) | 频繁删除,极少插入 |
二叉搜索树 | O(log N) | O(log N) | 需要动态平衡 |
二叉堆 | O(log N) | O(log N) | 综合性能最优(默认选择) |
三、二叉堆(Binary Heap)详解
1. 结构属性
- 完全二叉树:所有层除最后一层外必须填满,最后一层从左到右填充。
- 数组存储:索引从1开始,索引0为哨兵(存储极小值)。
- 节点
i
的父节点:i/2
(向下取整) - 节点
i
的子节点:左子2*i
,右子2*i+1
。
- 节点
示例数组与树的对应关系:
2. 堆序属性
- 最小堆:任意节点的值 ≤ 子节点的值(根为最小值)。
- 最大堆:任意节点的值 ≥ 子节点的值(根为最大值)。
Example
(FDS HW6)在一个包含 \(n\)(\(n > 1\))个元素的最大堆中,最小键值的数组索引可能是_______。
- A. 1
- B. \(\left\lfloor \frac{n}{2} \right\rfloor - 1\)
- C. \(\left\lfloor \frac{n}{2} \right\rfloor\)
- D. \(\left\lfloor \frac{n}{2} \right\rfloor + 2\)
这个题有一种很快的解法,因为最大堆的性质保证了越往下的子节点越小,那选索引最大的就好了。
要是具体思路分析的话,可以这么理解:叶子节点位于数组后半段;若非子节点包含最小值,则其子节点必须更小,违反最大堆定义。
3. 核心操作实现
3.1 插入(Insert)
步骤:
1. 新元素插入数组末尾(维持完全二叉树结构)。
2. 上滤(Percolate Up):与父节点比较,若更小则交换,直到满足堆序。
代码实现:
void Insert(ElementType X, PriorityQueue H) {
if (IsFull(H)) return Error;
int i = ++H->Size;
// 上滤:父节点 > X 时,父节点下移
for (; H->Elements[i/2] > X; i /= 2)
H->Elements[i] = H->Elements[i/2];
H->Elements[i] = X;
}
分步示例:插入元素 1
3.2 删除最小值(DeleteMin)
步骤:
1. 取出根节点(最小值)。
2. 将末尾元素移至根位置。
3. 下滤(Percolate Down):与较小的子节点交换,直到满足堆序。
代码实现:
ElementType DeleteMin(PriorityQueue H) {
if (IsEmpty(H)) return Error;
ElementType Min = H->Elements[1];
ElementType Last = H->Elements[H->Size--];
int i, Child;
for (i=1; i*2 <= H->Size; i=Child) {
Child = i*2;
// 选择较小的子节点
if (Child != H->Size && H->Elements[Child+1] < H->Elements[Child])
Child++;
if (Last > H->Elements[Child])
H->Elements[i] = H->Elements[Child];
else break;
}
H->Elements[i] = Last;
return Min;
}
分步示例:删除根节点 1
4. 其他操作
操作 | 实现方式 | 时间复杂度 |
---|---|---|
BuildHeap |
从最后一个非叶子节点开始下滤 | O(N) |
DecreaseKey |
降低某节点值后上滤 | O(log N) |
IncreaseKey |
增加某节点值后下滤 | O(log N) |
Delete |
先DecreaseKey 再DeleteMin |
O(log N) |
四、时间复杂度分析
1. 操作复杂度对比
操作 | 普通实现 | 二叉堆 | d堆(d=3) |
---|---|---|---|
Insert | O(1)/O(N) | O(log N) | O(log₃ N) |
DeleteMin | O(N) | O(log N) | O(3 log₃ N) |
2. 构建堆的时间复杂度证明
定理:对高度为h的完美二叉树,节点高度和为 \(2^{h+1} - (h+2)\)。
- 推导:
- 第k层有 \(2^k\) 个节点,每个节点高度为 \(h - k\)。
- 总高度和:\(\sum_{k=0}^h 2^k (h - k) = O(N)\)。
五、d-堆(d-ary Heap)
1. 特点
- 每个节点有d个子节点,树高降低为 \(\log_d N\)。
- 插入更快:上滤路径更短。
- 删除更慢:需比较d个子节点找最小值。
2. 代码调整
// 下滤时选择子节点范围:d*i - (d-2) 到 d*i +1
for (int j = d*i - (d-2); j <= d*i +1; j++) {
if (H->Elements[j] < minChild) {
minChild = H->Elements[j];
child = j;
}
}
六、总结
特性 | 二叉堆优势 | 适用场景 |
---|---|---|
插入/删除效率 | O(log N) 稳定 | 动态数据流 |
空间复杂度 | 数组存储,无指针开销 | 内存受限环境 |
扩展性 | 支持DecreaseKey等复杂操作 | 图算法、实时系统 |