堆及模板
堆及模板
1.堆的定义
堆是一颗完全二叉树。所谓完全二叉树就是指除了最后一层之外,其余层的节点都是满的。最后一层节点可以不满,但要依次从左到右排列。需要注意的是,如果最后一层也是满的,就称为满二叉树。
堆除了要满足完全二叉树的特征之外,还要满足以下两点的其中之一:
1. 树中每个节点的值都小于等于左右儿子节点的值。
2. 树中每个节点的值都大于等于左右儿子节点的值。
如果堆满足性质1,那么就称为小根堆。如果堆满足性质2,那么就称为大根堆。
根据上述性质,我们可以发现:
1. 如果是小根堆的话,那么整个堆中根节点的值是最小的。
2. 如果是大根堆的话,那么整个堆中根节点的值是最大的。
这里虽然介绍了大根堆和小根堆,但是本博客以小根堆为准来往下介绍。
2. 堆的存储
凡是一颗完全二叉树,都可以使用数组来表示/存储,堆也不例外。
假设,我们以下标1来表示根节点,节点x的左右孩子用下标表示就是:
1. x的左孩子:2x
2. x的右孩子:2x+1
具体可以看上图。
3. 堆的基本操作
堆除了可以求/删除最小值(小根堆),也可以求/删除最大值(大根堆)。
要想实现上图中的5个操作,我们还需要实现堆的两个最根本操作。
1. up(x)代表将节点x向上调整。
2. down(x)代表将节点x向下调整。
堆的这5个操作实际上都是基于这两大基本操作的。
4. down(x)函数细节
我们假设一开始就拥有了如上图所示的堆。我们现在将根节点的值改为6。如若此做,那么就不满足小根堆的条件了。因此,我们需要将根节点向下调整。思路如下:
1. 首先找到根节点以及其左右孩子的最小值,上图中是3,进行交换。交换后如下图所示。
2. 交换后,发现仍不满足小根堆条件。因此,求出6和6的左右孩子的最小值(这里是3),将3和6交换。交换后如下图所示。
3. 当交换到无法再进行交换时,整个完全二叉树就又变成堆了。
因此,down操作实际上就是:如果堆中的某个值变大了,我们就应该让变大的节点往下压。up操作相反。
5.up(x)函数细节
down操作介绍完后,我们需要再介绍以下up操作。假设我们将最后一个节点的值由5改为2。如上图所示。
更改完值之后,我们需要将这个节点往上浮,才能再次满足堆的条件。思路如下:
1. 将更改后的节点与其父亲节点进行比较。发现其父亲节点大于该节点,进行交换。如下图所示。
2. 再将2和其父亲节点(根节点)进行交换即可。如下图所示。
3. 当交换到不能再交换时,整个完全二叉树就又变成堆了。
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。当然,也可以不需要判断。直接up和down即可。(这里先up和down或先down再up没有任何区别)(因为,在这种情况下,up和down只会生效一个。)
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。其余内容均为作者原创。
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现