堆 一种用数组存储完全二叉树的数据结构

什么是堆

堆本质是一维数组,将完全二叉树的每个节点存入数组中,是一种比较特殊的存储形式,对于完全二叉树的第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;
}
posted @ 2022-01-08 21:13  moon_orange  阅读(391)  评论(0编辑  收藏  举报