堆 一种用数组存储完全二叉树的数据结构
什么是堆
堆本质是一维数组,将完全二叉树的每个节点存入数组中,是一种比较特殊的存储形式,对于完全二叉树的第n个节点,那么其左孩子节点是第2n个,右孩子节点是第2n+1个,于是我们可以根据该特性将第n个节点放入下标为n的数组中,整棵树也就可以完全存入数组。访问孩子节点直接根据公式访问下标即可。
注意这里存放的是完全二叉树,即除最后一层叶子节点们,以上所有层以满二叉树形式存储,最后一层的节点从左到右依次存储。只有这样才满足左孩子2n,右孩子2n+1的性质。
堆的作用
我们都听说过堆排序、优先队列、大根堆和小根堆等词汇,这些堆的根节点存放的是整个集合的最大/小值,我们可以从堆中取出该最值,那么堆会自行调整结构,使得新堆的根节点仍然是剩余集合的最值,因此我们每次取出的根节点是有序的。
对于大根堆来说,左右孩子节点都小于其父节点,这是一个递归的定义。
堆的调整
down操作:
堆具有自行调整根节点为集合最值的特性,以大根堆为例,如果修改其中某个节点的值,例如上图中的7修改为0,那么此时需要对0进行“下沉”操作,所谓下沉就是0和其左右孩子的最大值进行交换,直到0到达叶子节点。
up操作:
同理如果我们将7修改为11,就需要将11进行“上浮”操作,此时只需要与其父节点进行交换,直到11不再大于父节点。
down和up操作的次数都取决于树的高度,因此是logn的时间复杂度。
ps:如果需要取出根节点,那么就拿最后一个节点填入到根节点的位置,然后再对新的根节点进行down操作,使得堆重新满足定义。之所以拿最后一个节点,是因为最后一个节点属于叶子节点,没有孩子节点,否则移动其他节点需要增加维护孩子节点的成本。
堆的构建
对于一串包含n个数字的无序的数列,将其调整为堆的做法是:倒序的,依次将第n/2个到第1个数做down操作。整体时间复杂度为O(n)。
这里使用满二叉树作为特殊的完全二叉树进行证明,事实上,完全二叉树由于比满二叉树缺少部分叶子节点,时间复杂度会更低,但他们都有着上界O(N)。
堆的实现
参考例题:https://www.acwing.com/problem/content/840/
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int h[N], cnt;
// void up(int i) {
// if (i / 2 && h[i] < h[i / 2]) {
// heap_swap(i, i / 2);
// up(i / 2);
// }
// }
void down(int i) {
// i是根节点,u是孩子节点中较小的一个
int u = i;
if (i * 2 <= cnt && h[i * 2] < h[u]) u = i * 2;
if (i * 2 + 1 <= cnt && h[i * 2 + 1] < h[u]) u = i * 2 + 1;
if (h[u] != h[i]) {
swap(h[u], h[i]);
down(u);
}
}
int main() {
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
cnt = n;
// 构建堆
for (int i = n / 2; i > 0; i -- ) down(i);
while (m -- ) {
printf("%d ", h[1]);
// 取出堆的root
h[1] = h[cnt--];
down(1);
}
return 0;
}