堆及模板

堆及模板

1.堆的定义

img

    堆是一颗完全二叉树。所谓完全二叉树就是指除了最后一层之外,其余层的节点都是满的。最后一层节点可以不满,但要依次从左到右排列。需要注意的是,如果最后一层也是满的,就称为满二叉树。
    堆除了要满足完全二叉树的特征之外,还要满足以下两点的其中之一:
    1.  树中每个节点的值都小于等于左右儿子节点的值。
    2.  树中每个节点的值都大于等于左右儿子节点的值。
    如果堆满足性质1,那么就称为小根堆。如果堆满足性质2,那么就称为大根堆。
    根据上述性质,我们可以发现:
    1.  如果是小根堆的话,那么整个堆中根节点的值是最小的。
    2.  如果是大根堆的话,那么整个堆中根节点的值是最大的。
    这里虽然介绍了大根堆和小根堆,但是本博客以小根堆为准来往下介绍。

2. 堆的存储

img

    凡是一颗完全二叉树,都可以使用数组来表示/存储,堆也不例外。
    假设,我们以下标1来表示根节点,节点x的左右孩子用下标表示就是:
        1.  x的左孩子:2x
        2.  x的右孩子:2x+1
    具体可以看上图。

3. 堆的基本操作

img

    堆除了可以求/删除最小值(小根堆),也可以求/删除最大值(大根堆)。
    要想实现上图中的5个操作,我们还需要实现堆的两个最根本操作。
    1.  up(x)代表将节点x向上调整。
    2.  down(x)代表将节点x向下调整。
    堆的这5个操作实际上都是基于这两大基本操作的。

4. down(x)函数细节

img
img

    我们假设一开始就拥有了如上图所示的堆。我们现在将根节点的值改为6。如若此做,那么就不满足小根堆的条件了。因此,我们需要将根节点向下调整。思路如下:
        1. 首先找到根节点以及其左右孩子的最小值,上图中是3,进行交换。交换后如下图所示。
        2. 交换后,发现仍不满足小根堆条件。因此,求出66的左右孩子的最小值(这里是3),将36交换。交换后如下图所示。
        3. 当交换到无法再进行交换时,整个完全二叉树就又变成堆了。
    因此,down操作实际上就是:如果堆中的某个值变大了,我们就应该让变大的节点往下压。up操作相反。

img
img

5.up(x)函数细节

img

    down操作介绍完后,我们需要再介绍以下up操作。假设我们将最后一个节点的值由5改为2。如上图所示。
    更改完值之后,我们需要将这个节点往上浮,才能再次满足堆的条件。思路如下:
        1.  将更改后的节点与其父亲节点进行比较。发现其父亲节点大于该节点,进行交换。如下图所示。
        2.  再将2和其父亲节点(根节点)进行交换即可。如下图所示。
        3.  当交换到不能再交换时,整个完全二叉树就又变成堆了。

img
img

6. 堆的各种基本操作细节

    如何实现在堆中插入一个数?
        1.  先在堆的末尾添加一个数x
            heap[++size]  = x;
        2.  将这个数x向上调整
            up(size)
    如何求在堆中的最小值/最大值?
        heap[1]即可
    如何删除堆中的最小值/最大值?
        1.  先将堆的最后一个元素覆盖堆顶元素。(此时在堆中有两个最后一个元素)
            heap[1] = heap[size];
        2.  将堆的大小-1(将堆的最后一个元素删掉)。
            size--;
        3.  将堆顶元素向下调整即可。
            down(1);
    如何删除堆中的第k个元素?
        1.  先将堆的最后一个元素覆盖第k个元素。
            heap[k] = heap[size];
        2.  将堆的大小-1
            size--
        3.  分情况讨论,如果第k个元素比原先要大,那么就需要down,如果第k个元素比原先要小,那么就需要up。当然,也可以不需要判断。直接updown即可。(这里先updown或先downup没有任何区别)(因为,在这种情况下,updown只会生效一个。)
            up(k);
            down(k);
    如何修改堆中的第k个元素?
        1.  将堆中的第k个元素的值进行修改。
            heap[k] = x;
        2.  将修改后的节点进行调整。
            up(k);
            down(k);
    如何初始化堆?
        1.  遍历n/2~1,其中n/2代表堆中的最后一个中间节点。(1~n/2实际上就是堆中的所有中间节点)
        2.  在遍历的过程中,对每个中间节点进行down操作即可。

7. 堆模板

// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;

// 交换两个点,及其映射关系
void heap_swap(int a, int b)
{
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a], hp[b]);
    swap(h[a], h[b]);
}

void down(int u)
{
    int t = u;
    if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t)
    {
        heap_swap(u, t);
        down(t);
    }
}

void up(int u)
{
    while (u / 2 && h[u] < h[u / 2])
    {
        heap_swap(u, u / 2);
        u >>= 1;
    }
}

// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);

8. 例题

https://www.acwing.com/problem/content/840/
#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

int h[100010];
int s = 0;

//在这里关于堆都是以下标为基准进行操作

void down(int x){
    int p = x;
    if(2*x <= s && h[2*x] < h[p]){
        p = 2*x;
    }
    if(2*x+1 <= s && h[2*x+1] < h[p]){
        p = 2*x+1;
    }
    if(p != x){
        swap(h[p],h[x]);
        down(p);
    }
    
}


int main(){
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",&h[i]);
        s++;
    }
    //初始化小根堆
    for(int i=n/2;i>=1;i--){
        down(i);
    }
    for(int i=1;i<=m;i++){
        printf("%d ",h[1]);
        h[1] = h[s--];
        down(1);
    }
    return 0;
}
https://www.acwing.com/problem/content/841/
#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

int h[100010];
int s = 0;
//其中,ph存储的是堆中第几个插入的元素所对应的在堆中的下标是多少(有数组->堆)
//例如:ph[1] = 1;   代表堆中第一个插入的元素,在堆中的下标是1。
//需要注意的是:在堆中所有跟元素有关的操作都是跟下标有关,而跟该元素是第几个插入是无关的。
//例如:在堆中第三个插入的元素下标未必是3。
//其中,hp存储的是堆中下标所对应的元素是第几个插入的(堆->数组)
//例如:hp[1] = 1;   代表堆中下标为1的元素是第一个插入的。
int ph[100010],hp[100010];

void heap_swap(int a,int b){
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a],hp[b]);
    swap(h[a],h[b]);
}

void down(int x){
    int p = x;
    if(2*x <= s && h[2*x] < h[p]){
        p = 2*x;
    }
    if(2*x+1 <= s && h[2*x+1] < h[p]){
        p = 2*x+1;
    }
    if(p != x){
        //这里的交换,不光要交换值,而且也要交换映射关系
        heap_swap(p,x);
        down(p);
    }
}

void up(int x){
    while(x/2 && h[x/2] > h[x]){
        heap_swap(x/2,x);
        x = x/2;
    }
}

int main(){
    int n;
    char op[3];
    scanf("%d",&n);
    int count = 0;
    int x,k;
    while(n--){
        scanf("%s",op);
        if(op[0] == 'I'){
            scanf("%d",&x);
            //count记录的是该元素是第几个插入的
            //s代表堆的大小
            count++;
            s++;
            hp[s] = count;
            ph[count] = s;
            h[s] = x;
            up(s);
        }else if(op[0] == 'P' && op[1] == 'M'){
            printf("%d\n",h[1]);
        }else if(op[0] == 'D' && op[1] == 'M'){
            //当删除一个数时,映射关系也要改变
            heap_swap(1,s);
            s--;
            down(1);
        }else if(op[0] == 'D'){
            scanf("%d",&k);
            //取下标
            int number = ph[k];
            heap_swap(number,s);
            s--;
            up(number);
            down(number);
        }else{
            scanf("%d%d",&k,&x);
            int number = ph[k];
            h[number] = x;
            up(number);
            down(number);
        }
    }
    return 0;
}
    作者:gao79138
    链接:https://www.acwing.com/
    来源:本博客中的截图、代码模板及题目地址均来自于Acwing。其余内容均为作者原创。
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @   夏目^_^  阅读(30)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示