目录
一、概念
二叉堆:二叉堆是一棵满足“堆性质”的完全二叉树,树上的每个节点都有一个权值。若树中的任意一个结点的权值都小于等于其父节点的权值,则称该二叉树为满足“大根堆性质”的大根堆。若树中的任意一个节点的权值都大于等于其父节点的权值,则称该二叉树为满足“小根堆性质”的小根堆。
注:完全二叉树,其叶子节点都在最后两层,且在最后一层集中于左侧的二叉树
二、图示说明
1.二叉堆
如图1,小根堆,依据层次序列存储方式(逐层从左到右依次为树中的节点编号,把此编号作为节点在数组中存储位置的下标),将每个结点存入一个数组中。
![](https://img-blog.csdnimg.cn/154d7f572ed64e30b7e93f698448e1ea.png)
2.映射版二叉堆
如图2,ph[]中每个k映射到堆中的一个结点,而hp[]反过来从堆的每个结点映射到对应的k
![](https://img-blog.csdnimg.cn/cf95f60aad9b47789dbeefb9784894ef.png)
三、操作解释
1.二叉堆
数组模拟,小根堆
a.插入元素
如图3,4,插入一个元素4到堆中。先将该元素放在数组最后面,然后通过和其父节点比对,然后不断向上移。
![](https://img-blog.csdnimg.cn/0ed76253b3174550b02945f9d9a844be.png)
![](https://img-blog.csdnimg.cn/caf44f903ed149369c5c93dc8ba57002.png)
int h[N]; ///存储二叉堆
int si; ///存储二叉堆中最后一个结点的编号
void up(int x) {
///当结点x的父节点存在,且父节点大于节点x时满足条件
if (x / 2 && h[x / 2] > h[x]) {
swap(x, x / 2); ///将x节点和其父节点的权值进行交换
up(x / 2); ///再交换后,让X/2节点继续向上比较
}
}
cin >> x;
h[++si] = x; ///将x插入到数组h的最后一个位置
up(si); ///将插入的x的结点位置传入到up,让x向上比较上移
b.输出最小值
cout << h[1] << endl; ///堆中的第一个结点即为最小值
c.删除最小值
如图5,6,7,8,9所示,删除最小值。先将最小值和堆数组的最后一个元素进行交换,然后删除堆数组的最后一个元素,然后再对交换后的顶部结点进行down操作,和子节点进行比较来向下沉。
![](https://img-blog.csdnimg.cn/6c3f2f70db17437b80ca0f9abaf4597b.png)
![](https://img-blog.csdnimg.cn/0e074351e0364c5296c6770be0a7ea31.png)
![](https://img-blog.csdnimg.cn/3eeb897c98db4ccb9864a24effcf72ca.png)
![](https://img-blog.csdnimg.cn/87fcdcc351b6449891e419efc4d31f23.png)
![](https://img-blog.csdnimg.cn/fc0467bcb240430d9b5dca5f8de943c6.png)
void down(int x) {
int t = x;
///将节点x与其两个子节点进行比较,记录x最小值
if (2 * x <= si && h[t] > h[2 * x]) t = 2 * x;
if (2 * x + 1 <= si && h[t] > h[2 * x + 1]) t = 2 * x + 1;
if (t != x) { ///当最小值节点不为x时,将节点x与记录的t节点进行交换
heap_swap(t, x);
down(t); ///让当前节点t继续向下比较
}
}
swap(1, si); ///将节点1和最后一个节点进行权值交换
si--; ///将堆中的最后一个节点减去
down(1); ///将节点1向下比较
d.删除第k个节点
如图10,11,12,13,删除第2个节点。先将堆数组中第2个节点与最后一个节点进行交换,然后删除最后一个节点,最后让交换后的第2个节点进行向下比较。
![](https://img-blog.csdnimg.cn/e40f3d40b4f24b0aabb9ac78f6561529.png)
![](https://img-blog.csdnimg.cn/223fe1d524b84eb49c25103e01655898.png)
![](https://img-blog.csdnimg.cn/1305a4d5ff3948978a8cbabe54f37a24.png)
![](https://img-blog.csdnimg.cn/4cff9404ad704c56abe2329e86019c01.png)
void down(int x) {
int t = x;
///将节点x与其两个子节点进行比较,记录x最小值
if (2 * x <= si && h[t] > h[2 * x]) t = 2 * x;
if (2 * x + 1 <= si && h[t] > h[2 * x + 1]) t = 2 * x + 1;
if (t != x) { ///当最小值节点不为x时,将节点x与记录的t节点进行交换
heap_swap(t, x);
down(t); ///让当前节点t继续向下比较
}
}
void up(int x) {
///当结点x的父节点存在,且父节点大于节点x时满足条件
if (x / 2 && h[x / 2] > h[x]) {
heap_swap(x, x / 2); ///将x节点和其父节点的权值进行交换
up(x / 2); ///再交换后,让X/2节点继续向上比较
}
}
cin >>k;
swap(k, si); ///将节点k和最后一个节点进行权值交换
si--; ///将堆中的最后一个节点减去
down(k),up(k); ///将节点k,进行向下比较或向上比较(两个中只会进行一个)
2.映射版二叉堆
a.插入元素
如图14到图17所示,插入元素4。先将元素4加入堆数组的最后一个位置,然后建立k值到结点,结点到k值的映射关系,然后up向上比较元素4的父节点点。比较后发现,元素4比父节点小,故进行交换,先将k值到节点的关系进行交换,然后再将节点到k值的关系进行交换,最后再将两个节点的数值进行交换,然后向上比较发现无法替换,插入结束。
![](https://img-blog.csdnimg.cn/ea57eb24fd43496982158975b9470093.png)
![](https://img-blog.csdnimg.cn/087f9f12293a443d917929f53e3a5629.png)
![](https://img-blog.csdnimg.cn/e8e65edd4622404582661045d7c01727.png)
![](https://img-blog.csdnimg.cn/55889beb2a1d4727940267a75a334354.png)
int ph[N]; ///存储k值到节点的对应关系
int hp[N]; ///存储节点到k值的对应关系
int si; ///存储二叉堆中最后一个结点的编号
int idx; ///存储ph[]数组中k值的最后一个位置
void heap_swap(int a, int b) {
swap(ph[hp[a]], ph[hp[b]]); ///让a,b节点中,k值指向节点的对应关系调换
swap(hp[a], hp[b]); ///再让节点指向k值的对应关系调换
swap(h[a], h[b]); ///最后再将两个节点的权值进行调换
}
void up(int x) {
///当结点x的父节点存在,且父节点大于节点x时满足条件
if (x / 2 && h[x / 2] > h[x]) {
heap_swap(x, x / 2); ///将x节点和其父节点的权值进行交换,并修改对应关系
up(x / 2); ///再交换后,让X/2节点继续向上比较
}
}
h[++si] = x; ///将x插入到堆数组的最后一个位置
///将最后一个结点指向对应的k值,k值指向最后一个节点
hp[si] = ++idx, ph[idx] = si;
up(si); ///将插入的x的结点位置传入到up,让x向上比较上移
b.输出最小值
cout << h[1] << endl; ///堆中的第一个结点即为最小值
c.删除最小值
如图18到图22所示,删除最小值。先将顶部结点与最后一个结点及其映射关系交换,然后再将堆数组的最后一个节点去掉,然后再让顶部结点进行向下比较。
![](https://img-blog.csdnimg.cn/a7bf84b5a214402e86adbec22eecbf8c.png)
![](https://img-blog.csdnimg.cn/b05f7cc8b73b419e9b602dc837654e4f.png)
![](https://img-blog.csdnimg.cn/3409bb50680e464f9365e3a7ec1c1890.png)
![](https://img-blog.csdnimg.cn/f27d62f72944438ba4b02f2f77febf73.png)
![](https://img-blog.csdnimg.cn/cbbe5af5e6ce4c26bf789c76a93fd1f5.png)
void down(int x) {
int t = x;
///将节点x与其两个子节点进行比较,记录x最小值
if (2 * x <= si && h[t] > h[2 * x]) t = 2 * x;
if (2 * x + 1 <= si && h[t] > h[2 * x + 1]) t = 2 * x + 1;
if (t != x) { ///当最小值节点不为x时,将节点x与记录的t节点进行交换,并修改其对应关系
heap_swap(t, x);
down(t); ///让当前节点t继续向下比较
}
}
heap_swap(1, si); ///将节点1和最后一个节点进行权值交换,并调整对应关系
si--; ///将堆中的最后一个节点减去
down(1); ///将节点1向下比较
d.删除k对应的节点的值
如图23,24,25,26所示,删除值k9对应的节点。先让k9对应节点与最后一个节点交换权值,及其对应关系,然后删除最后一个节点,最后让交换前k9对应节点进行向下或向上比较。
![](https://img-blog.csdnimg.cn/f376fdfc2576473387c6ea911f91e949.png)
![](https://img-blog.csdnimg.cn/012a73ed4a91445ca283be82fac382dc.png)
![](https://img-blog.csdnimg.cn/ab7254de9dfb47908f7452513983399a.png)
![](https://img-blog.csdnimg.cn/71f09e5f8d9748ab9c2f570a6936445e.png)
cin >> k;
int t = ph[k]; ///记录输入k值对应的结点
heap_swap(t, si); ///交换k值对应节点和最后一个结点及其对应关系
si--; ///减去堆中最后一个节点
down(t),up(t);///让输入k值时,对应的结点进行向上或向下比较
e.修改k值对应节点的值
如图27,28,29所示,将k4对应的节点权值修改为4。先找到k4对应节点权值,然后将其修改为4,最后再让该节点进行向上或向下比较。
![](https://img-blog.csdnimg.cn/c03313742c1b4336a3db8772699408a7.png)
![](https://img-blog.csdnimg.cn/a66ba318bb884d2cba8b175deaf0de72.png)
![](https://img-blog.csdnimg.cn/c4754302c7934732bae93d342cac0d44.png)
cin >> k >> x;
h[ph[k]] = x; ///让k值所对应的节点权值修改为x
down(ph[k]),up(ph[k]); ///然后让k值所对应的节点,进行向上或向下比较
四、例题实践
1.堆
a.题目描述
题目来源:acwing
输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。
输入格式
第一行包含整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
输出格式
共一行,包含 m 个整数,表示整数数列中前 m 小的数。
数据范围
1≤m≤n≤1e5,
1≤数列中元素≤1e9
输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3
b.解题思路
思路:先将输入的数存入数组中,然后再从最后一个结点的父节点到第一个结点,每个结点进行向下比较。然后m次输出一个最小值,每一次输出完后,删除最小值。
c.代码实现
#include<iostream>
using namespace std;
const int N=1e5+10;
int h[N],si;
void down(int x){
int t=x;
if(2*x<=si&&h[t]>h[2*x]) t=2*x;
if(2*x+1<=si&&h[t]>h[2*x+1]) t=2*x+1;
if(x!=t){
swap(h[x],h[t]);
down(t);
}
}
int main(){
int n, m;
cin >>n >>m;
for(int i=1;i<=n;i++) cin >>h[i];
si=n;
for(int i=n/2;i>=1;i--) down(i); ///由最后一个节点的父节点开始,向下比较
while(m--){
cout <<h[1] <<' ';
///删除最小值
h[1]=h[si];
si--;
down(1);
}
return 0;
}
2.映射版堆
a.题目描述
维护一个集合,初始时集合为空,支持如下几种操作:
I x
,插入一个数 x;PM
,输出当前集合中的最小值;DM
,删除当前集合中的最小值(数据保证此时的最小值唯一);D k
,删除第 k 个插入的数;C k x
,修改第 k 个插入的数,将其变为 x;
现在要进行 N 次操作,对于所有第 2 个操作,输出当前集合的最小值。
输入格式
第一行包含整数 N。
接下来 N 行,每行包含一个操作指令,操作指令为 I x
,PM
,DM
,D k
或 C k x
中的一种。
输出格式
对于每个输出指令 PM
,输出一个结果,表示当前集合中的最小值。
每个结果占一行。
数据范围
1≤N≤1e5
−1e9≤x≤1e9
数据保证合法。
输入样例:
8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
输出样例:
-10
6
b.解题思路
思路:因为在题目中,要求输出能最小值,同时还要能修改k值对应节点的权值,所以采用映射版堆来维护该集合
c.代码实现
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int h[N]; ///存储二叉堆
int ph[N]; ///存储k值到节点的对应关系
int hp[N]; ///存储节点到k值的对应关系
int si; ///存储二叉堆中最后一个结点的编号
int idx; ///存储ph[]数组中k值的最后一个位置
char op[2];
int n;
void heap_swap(int a, int b) {
swap(ph[hp[a]], ph[hp[b]]); ///让a,b节点中,k值指向节点的对应关系调换
swap(hp[a], hp[b]); ///再让节点指向k值的对应关系调换
swap(h[a], h[b]); ///最后再将两个节点的权值进行调换
}
void down(int x) {
int t = x;
///将节点x与其两个子节点进行比较,记录x最小值
if (2 * x <= si && h[t] > h[2 * x]) t = 2 * x;
if (2 * x + 1 <= si && h[t] > h[2 * x + 1]) t = 2 * x + 1;
if (t != x) { ///当最小值节点不为x时,将节点x与记录的t节点进行交换,并修改其对应关系
heap_swap(t, x);
down(t); ///让当前节点t继续向下比较
}
}
void up(int x) {
///当结点x的父节点存在,且父节点大于节点x时满足条件
if (x / 2 && h[x / 2] > h[x]) {
heap_swap(x, x / 2); ///将x节点和其父节点的权值进行交换,并修改对应关系
up(x / 2); ///再交换后,让X/2节点继续向上比较
}
}
int main() {
cin >> n;
while (n--) {
cin >> op;
int k;
long long x;
if (op[0] == 'I') {
cin >> x;
h[++si] = x; ///将x插入到堆数组的最后一个位置
///将最后一个结点指向对应的k值,k值指向最后一个节点
hp[si] = ++idx, ph[idx] = si;
up(si); ///将插入的x的结点位置传入到up,让x向上比较上移
}
else if (op[0] == 'P') {
cout << h[1] << endl; ///堆中的第一个结点即为最小值
}
else if (op[0] == 'D' && op[1] == 'M') {
heap_swap(1, si); ///将节点1和最后一个节点进行权值交换,并调整对应关系
si--; ///将堆中的最后一个节点减去
down(1); ///将节点1向下比较
}
else if (op[0] == 'D' && op[1] != 'M') {
cin >> k;
int t = ph[k]; ///记录输入k值对应的结点
heap_swap(t, si); ///交换k值对应节点和最后一个结点及其对应关系
si--; ///减去堆中最后一个节点
down(t),up(t);///让输入k值时,对应的结点进行向上或向下比较
}
else {
cin >> k >> x;
h[ph[k]] = x; ///让k值所对应的节点权值修改为x
down(ph[k]),up(ph[k]); ///然后让k值所对应的节点,进行向上或向下比较
}
}
return 0;
}