堆与优先队列
阅读本章前,请先确保你懂得:
- 完全二叉树如何用数组存储
- 完全二叉树的深度
如果让你在一个数组中寻找最大值,你会怎么做?
\(n \le 10^5\)
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int main(){
int n,ans=0;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
ans=max(ans,a[i]);
}
cout<<ans;
return 0;
}
相信这个解法十分简单,那如果再加入 \(q\) 次插入操作呢?
\(q \le 10^5\)
也很简单,只需要不断的用原来数组中的最大值与新插入的数据比较就好了。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int main(){
int n,ans=0;
cin>>n>>q;
for(int i=1;i<=n;i++){
cin>>a[i];
ans=max(ans,a[i]);
}
while(q--){
int x;
cin>>x;
ans=max(ans,x);
}
cout<<ans;
return 0;
}
但是如果再加入删除最大值的操作呢?
此时情况就麻烦了,删除操作必须实现 \(O(n^2)\) 算法,而且此时插入操作也变成了 \(O(n^2)\),因为我们不能确定原数组中的最大值有没有重复。
所以总体的时间复杂度变成了 \(O(qn^2),明显超时\)
那有没有一种更优的算法呢?
当然有,那就是堆。
堆
如题目中所见
给定一个数列,初始为空,请支持下面三种操作:
- 给定一个整数 \(x\),请将 \(x\) 加入到数列中。
- 输出数列中最小的数。
- 删除数列中最小的数(如果有多个数最小,只删除 \(1\) 个)。
首先介绍一下堆这种数据结构。
堆是一种完全二叉树状的结构,无论如何总是将最值放在根节点的位置,这也就使得他的插入与删除操作都是 \(O(nlog_n)\) 的,而查询则是 \(O(1)\) 的。
既然最值永远在根的位置,那么每一棵子树也是如此。
插入操作
可以将插入的操作直接接到整棵树的最后,然后不断与根节点比较,如果比根节点还小,替换根节点,持续向上。
void up(int x){
while(x/2>=1&&h[x]<h[x/2]){//x/2代表x的根节点
swap(h[x],h[x/2]);
x/=2;
}
}
void push(int x){
h[++idx]=x;//idx表示当前的节点总数
up(idx);
}
删除操作
直接将根节点放到最后,在不断选择根节点,直到将整棵树补充完整。
void down(int x){
int t=x;
if(2*x<=idx&&h[2*x]<h[t]) t=2*x;//比较左节点
if(2*x+1<=idx&&h[2*x+1]<h[x]) x=2*x+1;//比较右节点
if(t!=x){
swap(h[x],h[t]);
down(t);//递归执行
}
}
void pop(){
h[1]=h[idx--];//相当于删除根节点
down(1);
}
查询
直接输出 \(h_1\)即可。
cout<<h[1];
于是完整代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int h[N],idx,n;
void down(int x){
int t=x;
if(2*x<=idx&&h[2*x]<h[t]) t=2*x;
if(2*x+1<=idx&&h[2*x+1]<h[x]) x=2*x+1;
if(t!=x){
swap(h[x],h[t]);
down(t);
}
}
void up(int x){
while(x/2>=1&&h[x]<h[x/2]){
swap(h[x],h[x/2]);
x/=2;
}
}
void push(int x){
h[++idx]=x;
up(idx);
}
void pop(){
h[1]=h[idx--];
down(1);
}
int main(){
cin>>n;
while(n--){
int op,x;
cin>>op;
if(op==1){
cin>>x;
push(x);
}
if(op==2){
cout<<h[1];
}
if(op==3){
pop();
}
}
return 0;
}
优先队列
类似 queue
,写作 priority_queue
。
手写堆太麻烦了,有没有更加便捷的方式?
C++ 的 STL 里有已经帮我们写好的堆模板,就是优先队列。
优先队列的操作很简单,和队列的操作是一样的。
priority_queue<T> q
: 创建一个类型为T
,名字为q
的优先队列。.push(x)
: 在队列里插入一个元素。.pop()
: 在队列里删除一个元素。.top()
: 返回优先队列里优先级最高的元素。.size()
: 返回队列长度。.empty()
: 若队列为空返回true
,否则返回false
。
优先队列默认为大根堆(即堆的根为数列中的最大值)如果要改成小根堆,那么需要加入一些参数。
priority_queue<T,vector<T>,greater<T>> q
或priority_queue<T,deque<T>,greater<T>> q
可以创建一个从大到小排序的堆。
greater<T>
能够将一个容器或函数的功能翻转,例如 sort
本来是从小到大排序,而 sort(a+1,a+n+1,greater<int>())
就能够实现从大到小排序。
结构体类型的优先队列
结构体有多个成员,因此难以有明确的最小值判断标准,这时就需要我们人为的定义小于号的定义,需要用到重载运算符:
struct node{
int a,b;//例如用a作为关键字比较
bool operator < (const node &x,const node &y){
return x.a<y.a;
}
}
然后再直接使用优先队列即可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)