前言
在深入理解堆的基础概念并实现其结构后,我们来看堆在实际工程中的两个核心应用:高效的堆排序算法和解决海量数据 TopK 问题的巧妙方法。
一、建堆策略
建堆是将无序数组转化为符合堆规则的完全二叉树的过程。相比于通过逐个插入元素(向上调整)需要 O(NlogN) 的时间复杂度,利用向下调整算法可以在 O(N) 时间内完成建堆,这是堆排序优化的关键前提。
1.1 向上调整算法回顾
向上调整通常用于插入新元素或维护堆性质。当子节点比父节点更'合适'时(例如小堆中子节点更小),子节点上浮直到找到正确位置。
核心逻辑:
- 新元素插入堆尾。
- 比较子节点与父节点,若不满足堆序则交换。
- 更新索引继续上浮,直至根节点或满足条件。
void AdjustUp(int* a, int child) {
int parent = (child - 1) / 2;
while (child > 0) {
if (a[child] < a[parent]) {
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
} else {
break;
}
}
}
若构建大顶堆,只需将比较符号反转即可。
1.2 向下调整算法与 O(N) 建堆
向下调整更适合整体建堆。利用完全二叉树的特性,从最后一个非叶子节点开始向前遍历,对每个节点执行向下调整。由于叶子节点天然满足堆性质,无需调整,因此只需关注非叶子节点。
时间复杂度分析: 虽然单次向下调整最坏为 O(logN),但考虑到不同高度节点的数量分布,整体建堆的复杂度实际为 O(N)。
void AdjustDown(int* a, int size, int parent) {
int child = parent * 2 + 1;
while (child < size) {
// 若有右孩子且右孩子更小,指向右孩子
if (child + 1 < size && a[child + 1] < a[child]) {
child++;
}
// 若子节点小于父节点,交换并继续下沉
if (a[child] < a[parent]) {
Swap(&a[child], &a[parent]);
parent = child;
child = parent * + ;
} {
;
}
}
}
{
( i = (size - - ) / ; i >= ; i--) {
AdjustDown(a, size, i);
}
}


