数据结构学习之~二叉堆 (P3378 【模版】堆)

P3378 【模板】堆

图片失效致歉

题目描述

给定一个数列,初始为空,请支持下面三种操作:

  1. 给定一个整数 x x x,请将 x x x 加入到数列中。
  2. 输出数列中最小的数。
  3. 删除数列中最小的数(如果有多个数最小,只删除 1 1 1 个)。

输入格式

第一行是一个整数,表示操作的次数 n n n。

接下来 n n n 行,每行表示一次操作。每行首先有一个整数 o p op op 表示操作类型。

  • 若 o p = 1 op = 1 op=1,则后面有一个整数 x x x,表示要将 x x x 加入数列。
  • 若 o p = 2 op = 2 op=2,则表示要求输出数列中的最小数。
  • 若 o p = 3 op = 3 op=3,则表示删除数列中的最小数。如果有多个数最小,只删除 1 1 1 个。

输出格式

对于每个操作 2 2 2,输出一行一个整数表示答案。

输入输出样例 #1

输入 #1

复制代码
5
1 2
1 5
2
3
2

输出 #1

复制代码
2
5

说明/提示

【数据规模与约定】

  • 对于 30 % 30\% 30% 的数据,保证 n ≤ 15 n \leq 15 n≤15。
  • 对于 70 % 70\% 70% 的数据,保证 n ≤ 10 4 n \leq 10^4 n≤104。
  • 对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 10 6 1 \leq n \leq 10^6 1≤n≤106, 1 ≤ x < 2 31 1 \leq x \lt 2^{31} 1≤x<231, o p ∈ { 1 , 2 , 3 } op \in \{1, 2, 3\} op∈{1,2,3}。

学习二叉堆的第一步:先找模版题

第二步:查资料oi wiki

万事先暴力,根据题目可以想到准备一个数组,插入时放到末尾,输出最小数时打擂台,时间复杂度是 O(n) ,删除时同理,插入为 O ( 1 ) O(1) O(1) ,算下来最坏情况每次操作都输出最小数为 O ( n 2 ) O(n^{2}) O(n2) ,也就是大约要运行 1e+12 次

正解部分

二叉堆是怎么做到在 O ( n l o g n ) O(n log n) O(nlogn) 插入, O ( n l o g n ) O(nlogn) O(nlogn) 删除,求最值只用 O ( 1 ) O(1) O(1) 的?

如图,这是一个二叉树(大根堆为例):

所以我们可以得知大根堆的特点:

  • 根绝对是这棵树的最大值

  • 子节点比父结点要小

  • 如果转化为一维平面,那么子节点的下标/2=父节点的下标(根没有父节点)

一维平面示例图:

x 1 2 3 4 5 6 7 8
wx 8 4 7 1 2 6 3 0

(二叉堆在数组中的储存方式)

1.top

还是以大根堆为例,无论是小根堆还是大根堆,他们的最值都是根(也就是数组里的w1

代码实现:

cpp 复制代码
int top(){
    return w[1];
}
2.push(a)操作

先将a放入二叉堆的末尾:

接着在与自己的父结点比较,如果 w[x]>w[x/2](即父结点小于子结点),就交换

所以一整个添加的过程如下:

代码如下:

cpp 复制代码
void xiufu_up(int x){
    if(x==1||x[x]<w[x/2]){
       return;
    }//已是根 或者 自己到了极限
    swap(w[x],w[x/2]);
    xiufu_up(x/2);//继续查找
}
void push(int x){
    w[++now]=x;//now=当前层数
    xiufu_up(now);//修复
}
3.pop()操作

所谓皇帝轮流做明年到我家,如果删除了一个结点,那么自己的子结点就要上来一个

我们可以想到一个思路:

从被删除的节点往下搜索,每次都判断自己的节点哪个更大,就让那个节点"替位",然而那个节点的位置又空了,所以再向子结点遍历... 整个过程很像递归:

5的节点3 4,4顶 -> 4的节点3 2,3顶 ->3的节点2,1。2顶

2返回(叶子结点)-> 3返回 -> 4返回 ->完成

代码实现:

cpp 复制代码
void xiufu_down(int x){
    if(x*2>tot){
       return;
    }
    int tmp=x*2;
    if(x*2+1<=now&&w[x*2]>w[x*2+1]){
       tmp++;
    }//自己的左结点比右结点大,则左结点登上宝座
    if(w[x]<w[tmp]){
       swap(w[x],w[tmp]);
       xiufu_down(tmp);
    }
}
void pop(){
    swap(w[1],w[now--]);
    xiufu_down();
}

我知道你有思路了,但你先别抄,这是大根堆的,原题讲的是最小的数,所以这道题是小根堆,前面的介绍学习只是让大家认识二叉堆的。所以我们现在换一种思路去做会有不同的发现:

绝对绝对绝对的正解(小根堆)?)

小根堆与大根堆截然不同:

  • 根绝对是这棵树的最小值

  • 子节点比父结点要大

  • 如果转化为一维平面,那么子节点的下标/2=父节点的下标(根没有父节点)

1.push and xiufu_up()

有了大根堆的思路我们也能写出代码:

cpp 复制代码
void xiufu(int x){
    if(x==1||a[x]>a[x/2]){
        return;
    }//如果为根或者比自己的父结点大(符合从小到大)
    swap(a[x],a[x/2]);
    xiufu(x/2);//继续以父结点(子节点)的位置修复
}//修复堆
void push(int x){
    a[++now]=x;//要++now
    xiufu(now);//自底向上
}//添加数
2.pop and xiufu_down()

实际上大根堆转化为小根堆不难,就是反过来嘛 找规律

cpp 复制代码
void xiufupop(int x){
    if(x*2>now){
      return;  
    }//自己是叶子
    int tmp=x*2;
    if(x*2+1<=now&&a[x*2]>a[x*2+1]){
        tmp++;
        //tmp=x*2+1
        //变更为右节点
    }
    if(a[x]>a[tmp]){
        swap(a[x],a[tmp]);
        xiufupop(tmp);
    }
}//修复
void pop(){
    swap(a[1],a[now--]);
    xiufupop(1);
}

AC代码(手动模拟法)

请勿"ctrl+c,ctrl+v"学

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int a[1000001];
int now;//当前层数

int n,op,x;
int top(){
    return a[1];
}
void xiufu(int x){
    if(x==1||a[x]>a[x/2]){
        return;
    }//如果为根或者比自己的父结点大(符合从小到大)
    swap(a[x],a[x/2]);
    xiufu(x/2);//继续以父结点(子节点)的位置修复
}//修复堆
void push(int x){
    a[++now]=x;//要++now
    xiufu(now);//自底向上
}//添加数
void xiufupop(int x){
    if(x*2>now){
      return;  
    }//自己是叶子
    int tmp=x*2;
    if(x*2+1<=now&&a[x*2]>a[x*2+1]){
        tmp++;
        //tmp=x*2+1
        //变更为右节点
    }
    if(a[x]>a[tmp]){
        swap(a[x],a[tmp]);
        xiufupop(tmp);
    }
}//修复
void pop(){
    swap(a[1],a[now--]);
    xiufupop(1);
}
int main(){
    cin>>n;
    now=0;
    for(int i=1;i<=n;i++){
        cin>>op;
        if(op==1){
            cin>>x;
            push(x);
        }else if(op==2){
            cout<<top()<<"\n";
        }else{
            pop();
        }
    }
    return 0;
}

STL容器做法

c++提供了 priority_queue 的 STL 容器,具体用法可以去搜:

AC 代码( STL 法):

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
int n;
int op,x;
int main(){
    cin>>n;
     priority_queue<int,vector<int>,greater<int>>q;
    for(int i=1;i<=n;i++){
        cin>>op;
        if(op==1){
            cin>>x;
            q.push(x);
        }else if(op==2){
            cout<<q.top()<<"\n";
        }else{
            q.pop();
        }
    }
    return 0;
}
相关推荐
云泽8082 小时前
笔试算法 - 链表篇(一):移除、反转、合并、回文判断全解析
数据结构·c++·算法·链表
也曾看到过繁星2 小时前
数据结构-复杂度
数据结构
菜菜的顾清寒2 小时前
HOT力扣100(43)二叉树-翻转二叉树
数据结构·算法·leetcode
小poop2 小时前
深入理解指针(中):数组与指针的进阶之旅
c++
z200509302 小时前
【Linux学习】Linux中的进程程序替换
linux·服务器·学习
小+不通文墨2 小时前
把树莓派外接的DHT11接收的温湿度发送到emqx上
经验分享·笔记·嵌入式硬件·学习·树莓派
bush42 小时前
嵌入式linux学习记录四
linux·运维·学习
朔北之忘 Clancy3 小时前
2026 年 3 月青少年软编等考 C/C++ 一级真题解析
c语言·开发语言·c++·青少年编程·题解·考级