一、基本概念
堆(Heap) 是一种特殊的 完全二叉树 结构,满足以下性质:
-
最大堆(Max-Heap) :每个父节点的值 大于或等于 其子节点的值。
-
最小堆(Min-Heap) :每个父节点的值 小于或等于 其子节点的值。
-
堆顶元素:根节点是堆中最大(或最小)的元素。
二、堆的性质
-
完全二叉树结构:所有层级除最后一层外完全填充,最后一层节点从左到右排列。
-
数组存储 :堆通常用 数组 实现,无需指针,节省内存
-
父节点索引
i
的左子节点:2i + 1
-
父节点索引
i
的右子节点:2i + 2
-
子节点索引
i
的父节点:⌊(i-1)/2⌋
在竞赛中,以上存储方式会不可避免的有边界问题,所以在存储中,通常以数组的下标为 1为起始进行存储 ,这样一来会使代码更加简洁,容易理解,节省时间。
-
父节点索引
i
的左子节点:2i
-
父节点索引
i
的右子节点:2i + 1
-
子节点索引
i
的父节点:⌊i / 2⌋
三、核心操作与时间复杂度
了解堆,我们先了解其中向上调整算法(up
操作)和向下调整算法(down
操作)是堆排序的核心操作,下面将分别对这两种算法进行详细介绍。
向下调整算法(down
操作)
原理
向下调整算法用于维护堆的性质,当一个节点的值发生变化(通常是变大或变小),可能破坏了堆的性质时,需要将该节点向下调整到合适的位置。具体做法是比较该节点与其子节点的值,如果不满足堆的性质(大顶堆中父节点小于子节点,小顶堆中父节点大于子节点),则将该节点与较大(大顶堆)或较小(小顶堆)的子节点交换位置,然后继续对交换后的节点进行向下调整,直到满足堆的性质或到达叶子节点。
cpp
void down(int u){//向下调整算法
int t = u;
if (u * 2 <= size && heap[u * 2] < heap[t]) t = u * 2;
if (u * 2 + 1 <= size && heap[u * 2 + 1] < heap[t]) t = u * 2 + 1;
if (u != t){
swap(heap[u], heap[t]);
down(t);// 递归到叶子节点
}
}
复杂度分析
- 时间复杂度:O(log n),每次向下调整最多需要遍历树的高度,完全二叉树的高度为 log n。
- 空间复杂度:O(log n),递归调用栈的深度为树的高度。
向上调整算法(up
操作)
原理
向上调整算法通常用于在向堆中插入新元素时维护堆的性质。当新元素插入到堆的末尾时,可能破坏了堆的性质,需要将该元素向上调整到合适的位置。具体做法是比较该节点与其父节点的值,如果不满足堆的性质,则将该节点与父节点交换位置,然后继续对交换后的节点进行向上调整,直到满足堆的性质或到达根节点。
cpp
void up(int u){//向上调整算法
while(u / 2 && heap[u / 2] > heap[u]){
swap(heap[u / 2], heap[u]);
u /= 2;//调整当前节点位置,u 表示当前节点
}
}
复杂度分析
- 时间复杂度:O(log n),因为每次向上调整最多需要遍历树的高度。
- 空间复杂度:O( 1 ),只需要常数级的额外空间。
插入元素
cpp
int sz, temp[N];
void insert(int x, int temp[]){
temp[++ sz] = x;
up(sz);
}
从堆尾加入元素,然后通过向上调整算法慢慢上浮。
删除堆顶
cpp
void heap_pop(){
heap[1] = heap[size --];
down(1);
}
堆尾元素将顶元素覆盖,然后在进行向下调整,慢慢下沉以维持堆结构特性。
构建堆
cpp
void heapify(int n){
for(int i = 1; i <= n; i ++) cin >> heap[i];
size = n;
for (int i = n / 2; i; i --) down(i);
}
堆化,又称"Floyd算法"(没错他真的叫"Floyd算法",还有一个关于最短路径的"Floyd - Warshall 算法",没错,也是他,这个老哥是有点实力在身上的),堆化利用完全二叉树的特性,从最后一个非叶子节点开始进行向下调整,叶子节点自身天然满足堆的局部性质 。
获取堆顶
cpp
int heap_top(int a[]){
return a[1];
}
堆是由下标1开始存储,所以a[ 1 ] 是堆顶元素。
操作 | 描述 | 时间复杂度 |
---|---|---|
插入元素 | 添加到末尾,通过 上浮(Heapify Up) 调整 | O(log n) |
删除堆顶 | 用末尾元素替换堆顶,通过 下沉(Heapify Down) 调整 | O(log n) |
构建堆 | 将无序数组调整为堆(从最后一个非叶子节点开始调整) | O(n) |
获取堆顶 | 直接访问根节点 | O(1) |
四、堆的典型应用
-
堆排序(Heap Sort):利用堆实现的高效排序算法,时间复杂度 O(n log n)。
-
Top K 问题:高效获取数据流中最大/最小的 K 个元素。
堆排序
cpp
void downAdjust(int size, int u, int a[]){
int t = u;
if (u * 2 <= size && a[2 * u] < a[t]) t = 2 * u;
if (u * 2 + 1 <= size && a[2 * u + 1] < a[t]) t = 2 * u + 1;
if(t != u){
swap(a[t], a[u]);
downAdjust(size, t, a);
}
}
void heap_sort(int size){
while(size){
swap(heap[1], heap[size --]);
downAdjust(size, 1, heap);
}
}
将堆顶元素和堆尾元素互换,然后将互换后的新堆顶元素下沉到符合堆结构特性。可以看作是取出堆顶的最值元素放到堆尾,然后对剩余元素进行重复操作,循环size 次,size 次后有序。"升序大根堆,降序小根堆。"
Top K 问题
cpp
void top_k(int heap[]){
int k;
cout << "please input the K" << endl;
cin >> k;
while(k --){
cout << heap_top(heap) << " ";
heap_pop();
}
}
由于堆的特性,堆顶元素是最值,top K问题,高效获取数据流中最大/最小的 K 个元素。取出堆顶元素,然后删除堆顶元素, 依次循环 K 次。