目录
想对优先队列有个初步的了解可以看我的这篇博客:
优先队列
大佬文章
想了解堆的基础推荐看b站上的这个视频:
堆的定义
以及这篇博客:
堆
堆的定义
数据结构中的堆:
堆是一种特殊的完全二叉树,其中每个节点的值都大于等于(大顶堆)或小于等于(小顶堆)其子节点的值。满足以下性质:
完全二叉树
关于完全二叉树我要多提一嘴,,,
完全二叉树就是:其每个节点都与高度为h的满二叉树中编号为1~n的节点一 一对应.
即(除了最后一层外,其他层都填满,且最后一层的节点都靠左排列的二叉树)。
特点:
1.只有最后两层有叶子节点
2.最多只有一个度为1的节点
如图所示
以下都是完全二叉树
1. 大顶堆
定义
- 每个节点的值都大于或等于其子节点的值
- 根节点是整个堆中的最大值
- 也称为"最大堆"
性质
- 父节点 ≥ 子节点
- 堆顶元素始终是当前堆中的最大值
优先队列实现:priority_queue<int,vector< int > >q;
以下是一个大顶堆的示例(用数组表示并可视化):
数组表示: [9, 7, 5, 6, 3, 2, 4]
树形结构:
9
/ \
7 5
/ \ / \
6 3 2 4
每个节点的值 ≥ 其子节点的值(例如:9 ≥ 7和5,7 ≥ 6和3,依此类推)。
2.小顶堆
定义
每个节点的值都小于或等于其子节点的值
根节点是整个堆中的最小值
也称为"最小堆"
性质
- 父节点 ≤ 子节点
- 堆顶元素始终是当前堆中的最小值
优先队列实现:priority_queue<int,vector< int >,greater< int > >q;
以下是一个小顶堆的示例(用数组表示并可视化):
数组表示: [1, 3, 2, 6, 4, 5]
树形结构:
1
/ \
3 2
/ \ /
6 4 5
每个节点的值 ≤ 其子节点的值(例如:1 ≤ 3和2,3 ≤ 6和4,2 ≤ 5)。
今天重点要学习的是对顶堆
为什么要介绍大顶堆和小顶堆呢,因为今天这篇文章重点介绍对顶堆,,以及如何让用对顶堆求中位数,怎么求中位数。而对顶堆由大顶堆和小顶堆组成,!!!有了上述的基础,相信大家一定对堆和优先队列有了一定的了解!那么我们继续往下学习吧~
3. 对顶堆
定义
- 由一个大顶堆和一个小顶堆组合而成
- 也称为"双堆"或"中位数堆"
典型结构
大顶堆(左半部分) | 小顶堆(右半部分)
存储较小的一半数据 | 存储较大的一半数据
堆顶 是 左半 最大值 | 堆顶是右半最小值
操作规则
-
大顶堆存储较小的一半元素
-
小顶堆存储较大的一半元素
-
保持两个堆的大小平衡(数量相等或大顶堆多1个)
例题训练(堆)
模板
这里为大家整理了一份模板
#include<bits/stdc++.h>
using namespace std;
priority_queue<int> l; // 大顶堆(存较小的一半,左半部分)
priority_queue<int, vector<int>, greater<int>> r; // 小顶堆(存较大的一半,右半部分)
void addNum(int num) {
// 插入规则:num ≤ l.top() 则插入左边,否则插入右边
if (l.empty() || num <= l.top()) {
l.push(num);
} else {
r.push(num);
}
// 平衡规则:确保 l.size() == r.size() 或 l.size() == r.size() + 1
if (l.size() > r.size() + 1) {
r.push(l.top());
l.pop();
} else if (r.size() > l.size()) {
l.push(r.top());
r.pop();
}
}
int midNum() {
if (l.size() == r.size()) {
return (l.top() + r.top()) / 2; // 偶数个元素,取平均值
} else {
return l.top(); // 奇数个元素,左边堆顶就是中位数
}
}
// 示例用法
int main() {
vector<int> nums = {4, 2, 6, 1, 5};
for (int num : nums) {
addNum(num);
cout << "当前中位数: " << midNum() << endl;
}
return 0;
}
解释说明
-
变量名定义:
l
(left):大顶堆,存储 较小的一半数字(堆顶是最大值)。r
(right):小顶堆,存储 较大的一半数字(堆顶是最小值)。
-
插入规则:
- 新数字 ≤
l.top()
→ 插入l
(左边堆)。 - 否则插入
r
(右边堆)。
- 新数字 ≤
-
平衡规则:
l
的大小不能比r
大超过 1,否则把l
的堆顶移到r
。r
的大小不能超过l
,否则把r
的堆顶移到l
。
-
中位数计算:
- 偶数个数字 →
(l.top() + r.top()) / 2
。 - 奇数个数字 →
l.top()
(因为l
的大小比r
大 1)。
- 偶数个数字 →
运行过程
nums[]:[4, 2, 6, 1, 5]
插入数字 | 堆状态(l | r) | 中位数计算 | 中位数 |
---|---|---|---|---|
4 | [4] | [] | l.top() | 4 |
2 | [2,4] | [] → 平衡→ [2] [4] | (2+4)/2 | 3 |
6 | [2] | [4,6] → 平衡→ [2,4] [6] | 4 | 4 |
1 | [1,2,4] | [6] → 平衡→ [1,2] [4,6] | (2+4)/2 | 3 |
5 | [1,2] | [4,5,6] → 平衡→ [1,2,4] [5,6] | 4 | 4 |
最终输出(全整数中位数):
插入 4 后中位数: 4
插入 2 后中位数: 3
插入 6 后中位数: 4
插入 1 后中位数: 3
插入 5 后中位数: 4
295. 数据流的中位数
题目来源
思路
一道模板题,根据所给的函数意思去写,注意数据类型用double就可以了。
AC代码
class MedianFinder {
public:
priority_queue<int,vector<int>>l;
priority_queue<int,vector<int>,greater<int>>r;
MedianFinder() { }
void addNum(int num) {
if(l.size()==0||num<=l.top())
l.push(num);
else
r.push(num);
if(l.size()>r.size()+1)
r.push(l.top()),l.pop();
else if(l.size()<r.size())
l.push(r.top()),r.pop();
}
double findMedian() {
double x;
if(l.size()==r.size())
x=(l.top()+r.top())/2.0;
else
x=l.top();
return x;
}
};
P1168 中位数
题目描述
给定一个长度为 N N N 的非负整数序列 A A A,对于前奇数项求中位数。
输入格式
第一行一个正整数 N N N。
第二行 N N N 个正整数 A 1 … N A_{1\dots N} A1…N。
输出格式
共 ⌊ N + 1 2 ⌋ \lfloor \frac{N + 1}2\rfloor ⌊2N+1⌋ 行,第 i i i 行为 A 1 … 2 i − 1 A_{1\dots 2i - 1} A1…2i−1 的中位数。
输入 #1
7
1 3 5 7 9 11 6
输出 #1
1
3
5
6
输入 #2
7
3 1 5 9 8 7 6
输出 #2
3
3
5
6
说明/提示
对于 20 % 20\% 20% 的数据, N ≤ 100 N \le 100 N≤100;
对于 40 % 40\% 40% 的数据, N ≤ 3000 N \le 3000 N≤3000;
对于
100
%
100\%
100% 的数据,
1
≤
N
≤
100000
1 \le N ≤ 100000
1≤N≤100000,
0
≤
A
i
≤
1
0
9
0 \le A_i \le 10^9
0≤Ai≤109。
思路
很简单的一道模板题,依照上诉介绍,我们可以分别定义一个大顶堆用来存储较小的一半数,和一个小顶堆用来存较大的一半数,因为运用优先队列可自动排序,每第奇数个中位数就是大顶堆的堆顶,因为当二者长度之和等于奇数时,大顶堆的长度一定比小顶堆的长度大1。
代码注释很详细,这里不多做赘述。
AC代码
#include<bits/stdc++.h>
using namespace std;
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
#define int long long
#define PII pair<int,int>
#define fi first
#define se second
#define endl '\n'
const int N=1e5+6;
int n,a[N];
priority_queue<int,vector<int>>l;//大顶堆 ,用来存放较小的一半数,顺序从大到小
priority_queue<int,vector<int>,greater<int>>r;//小顶堆 ,用来存放较大的一半数,顺序从小到大
void solve()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
if(l.size()==0||a[i]<l.top())//如果大顶堆长度为0,或者带判元素小于大顶堆堆顶的数
l.push(a[i]);//就存入大顶堆中,即较小的一半数
else
r.push(a[i]);//否则存入小顶堆中
//调整两堆大小,即长度相等,或大顶堆长1.
if(l.size()>r.size()+1)//如果大顶堆的长度比小顶堆的长度加一还大
r.push(l.top()),l.pop();//就要将其堆顶元素移到小顶堆中
else if(l.size()<r.size())//如果大顶堆的长度比小顶堆小
l.push(r.top()),r.pop();//就要将小顶堆的堆顶元素移到大顶堆中
if(i%2==1)//每第奇数个元素输出当前的中位数,即大顶堆堆顶
cout<<l.top()<<endl;
}
}
signed main()
{
IOS;
int _=1;
// cin>>_;
while(_--)
solve();
return 0;
}
106. 动态中位数
题目来源
思路
这也是一道标准的板子题,只不过该题的输出需要特别注意一下,可以定义一个数组存放每个中位数,按照题目要求输出时要特别注意,每当满10个,就要换行,同时,如果最后一行的数据量少于10个也要输出换行!!!
AC代码
#include<bits/stdc++.h>
using namespace std;
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
#define int long long
#define PII pair<int,int>
#define fi first
#define se second
#define endl '\n'
const int N=1e5+6;
int n,m,a[N];
void solve()
{
priority_queue<int,vector<int>>l;//大顶堆,存较小的一半数,堆顶最大
priority_queue<int,vector<int>,greater<int>>r;//小顶堆,存较大的一半数,堆顶最小
cin>>n>>m;
vector<int>v;//存中位数
for(int i=1;i<=m;i++)
{
cin>>a[i];
if(l.empty()||a[i]<l.top())//进入大顶堆的条件
l.push(a[i]);
else
r.push(a[i]);
//若长度不协调,需要调整调整
if(l.size()>r.size()+1)
{
r.push(l.top());
l.pop();
}
else if(l.size()<r.size())
{
l.push(r.top());
r.pop();
}
if(i%2)
v.push_back(l.top());
}
cout<<n<<" "<<v.size()<<endl;
for(int i=0;i<v.size();i++)
{
cout<<v[i]<<" ";
if((i+1)%10==0)
cout<<endl;
}
if(v.size()%10)
cout<<endl;
}
signed main()
{
IOS;
int _=1;
cin>>_;
while(_--)
solve();
return 0;
}
P1801 黑匣子
题目描述
Black Box 是一种原始的数据库。它可以储存一个整数数组,还有一个特别的变量 i i i。最开始的时候 Black Box 是空的.而 i = 0 i=0 i=0。这个 Black Box 要处理一串命令。
命令只有两种:
-
ADD(x)
:把 x x x 元素放进 Black Box; -
GET
: i i i 加 1 1 1,然后输出 Black Box 中第 i i i 小的数。
记住:第 i i i 小的数,就是 Black Box 里的数的按从小到大的顺序排序后的第 i i i 个元素。
我们来演示一下一个有11个命令的命令串。(如下表所示)
序号 | 操作 | i i i | 数据库 | 输出 |
---|---|---|---|---|
1 | ADD(3) | 0 0 0 | 3 3 3 | / |
2 | GET | 1 1 1 | 3 3 3 | 3 3 3 |
3 | ADD(1) | 1 1 1 | 1 , 3 1,3 1,3 | / |
4 | GET | 2 2 2 | 1 , 3 1,3 1,3 | 3 3 3 |
5 | ADD(-4) | 2 2 2 | − 4 , 1 , 3 -4,1,3 −4,1,3 | / |
6 | ADD(2) | 2 2 2 | − 4 , 1 , 2 , 3 -4,1,2,3 −4,1,2,3 | / |
7 | ADD(8) | 2 2 2 | − 4 , 1 , 2 , 3 , 8 -4,1,2,3,8 −4,1,2,3,8 | / |
8 | ADD(-1000) | 2 2 2 | − 1000 , − 4 , 1 , 2 , 3 , 8 -1000,-4,1,2,3,8 −1000,−4,1,2,3,8 | / |
9 | GET | 3 3 3 | − 1000 , − 4 , 1 , 2 , 3 , 8 -1000,-4,1,2,3,8 −1000,−4,1,2,3,8 | 1 1 1 |
10 | GET | 4 4 4 | − 1000 , − 4 , 1 , 2 , 3 , 8 -1000,-4,1,2,3,8 −1000,−4,1,2,3,8 | 2 2 2 |
11 | ADD(2) | 4 4 4 | − 1000 , − 4 , 1 , 2 , 2 , 3 , 8 -1000,-4,1,2,2,3,8 −1000,−4,1,2,2,3,8 | / |
现在要求找出对于给定的命令串的最好的处理方法。ADD
命令共有
m
m
m 个,GET
命令共有
n
n
n 个。现在用两个整数数组来表示命令串:
-
a 1 , a 2 , ⋯ , a m a_1,a_2,\cdots,a_m a1,a2,⋯,am:一串将要被放进 Black Box 的元素。例如上面的例子中 a = [ 3 , 1 , − 4 , 2 , 8 , − 1000 , 2 ] a=[3,1,-4,2,8,-1000,2] a=[3,1,−4,2,8,−1000,2]。
-
u 1 , u 2 , ⋯ , u n u_1,u_2,\cdots,u_n u1,u2,⋯,un:表示第 u i u_i ui 个元素被放进了 Black Box 里后就出现一个
GET
命令。例如上面的例子中 u = [ 1 , 2 , 6 , 6 ] u=[1,2,6,6] u=[1,2,6,6] 。输入数据不用判错。
输入格式
第一行两个整数
m
m
m 和
n
n
n,表示元素的个数和 GET
命令的个数。
第二行共 m m m 个整数,从左至右第 i i i 个整数为 a i a_i ai,用空格隔开。
第三行共 n n n 个整数,从左至右第 i i i 个整数为 u i u_i ui,用空格隔开。
输出格式
输出 Black Box 根据命令串所得出的输出串,一个数字一行。
输入 #1
7 4
3 1 -4 2 8 -1000 2
1 2 6 6
输出 #1
3
3
1
2
说明/提示
- 对于 30 % 30\% 30% 的数据, 1 ≤ n , m ≤ 1 0 4 1 \leq n,m \leq 10^{4} 1≤n,m≤104。
- 对于 50 % 50\% 50% 的数据, 1 ≤ n , m ≤ 1 0 5 1 \leq n,m \leq 10^{5} 1≤n,m≤105。
- 对于 100 % 100\% 100% 的数据, 1 ≤ n , m ≤ 2 × 1 0 5 , ∣ a i ∣ ≤ 2 × 1 0 9 1 \leq n,m \leq 2 \times 10^{5},|a_i| \leq 2 \times 10^{9} 1≤n,m≤2×105,∣ai∣≤2×109,保证 u u u 序列单调不降。
思路
虽然让求第i小的数,但是同样可用对顶堆,开左右两个堆,左堆为大根堆,右堆为小根堆(方便转移),维护这两个堆:
- 左堆的元素数量维持在i,那么它的堆顶就是我们要输出的第i小的元素
- 用一个变量j记录此时队列中元素的数量,当j小于等于ui时,要向队列中增加元素,同时j++。
- 直到左堆的长度大于了i,这时需要将左堆堆顶移到右堆中。
- 这样一个过程进行后,输出左堆堆顶,就是第i小的元素。
- 最后要将右堆的一个元素移到左堆去
AC代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e5+5;
int n,m,a[N],u;
priority_queue<int>l;//左堆,用来存前i个数
priority_queue<int, vector<int>, greater<int>>r;//右堆,便于左堆的转移
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1,j=1;i<=m;i++)
{
cin>>u;
while(j<=u)//当j小于等于ui时,要向队列中增加元素
{
l.push(a[j]);
j++;
if(l.size()>i) r.push(l.top()),l.pop();//左堆的长度大于了i需要处理
}
cout<<l.top()<<endl;//左堆堆顶就是此时第i小的数
if(r.size()) l.push(r.top()),r.pop();
}
return 0;
}
例题训练(优先队列)
264. 丑数 II
题目来源
思路
这道题在上一篇写过一个类似的,可以看这篇博客的T3473题的优先队列(小顶堆)解法。
每个丑数肯定都是由一个丑数与2,3,5相乘得到的,2,3,5本身也是丑数。首先定义变量x初始值为1,将每个丑数入队,与2,3,5相乘得到新的丑数后,再从队列踢出,将x赋值为新的队首元素,也就是当前最小丑数,这样再与2,3,5相乘又得到新的一些丑数,这样将丑数踢出n-1次时,队首元素就是第n个丑数。
这道题要注意数据范围,队列的数据类型要开long long
AC代码
class Solution {
public:
int nthUglyNumber(int n) {
//建立小顶堆,这样数就会按从小到大排列
priority_queue<long long,vector<long long>,greater<long long>>q;
long long x=1;//x初值为1,因为2,3,5本身也是符合的丑数
vector<int>v;//用一个数组存放2,3,5
v.push_back(2);
v.push_back(3);
v.push_back(5);
for(int i=1;i<n;i++)//弹出第n-1个后就是第n个
{
for(int j=0;j<v.size();j++)
q.push(v[j]*x);//将队首最小的丑数与vi相乘,得到下一个丑数
x=q.top();//x赋值每个队首元素
while(!q.empty()&&q.top()==x)//若队首元素等于x,就弹出,这样就会更新新的x值
q.pop();
}
return x;
}
};
506. 相对名次
题目来源
思路
这道题卡了我一会,想复杂了一开始用对顶堆,其实只需要用优先队列,自动从降序排序后,再用map给相应的值标记就好了。
AC代码
class Solution {
public:
vector<string> findRelativeRanks(vector<int>& s) {
priority_queue<int,vector<int>>q;
int i;
map<int,string>mp;
for(i=0;i<s.size();i++)
q.push(s[i]);
i=1;
while(!q.empty())
{
//1~3是特殊的,单独特判并赋值。
if(i==1)
mp[q.top()]="Gold Medal";
else if(i==2)
mp[q.top()]="Silver Medal";
else if(i==3)
mp[q.top()]="Bronze Medal";
else
mp[q.top()]=to_string(i);//记得将数字转化为字符串。
q.pop();
i++;
}
vector<string>v;
for(int i=0;i<s.size();i++)
v.push_back(mp[s[i]]);
return v;
}
};
703. 数据流中的第 K 大元素
题目来源
思路
很模板,按照要求入队,只要长度大于了k就出队。
AC代码
class KthLargest {
public:
KthLargest(int k, vector<int>& nums) {
for(int i=0;i<nums.size();i++)//先将数组元素入队
{
q.push(nums[i]);
if(q.size()>k)q.pop();//如果队列长度大于k了就出队
}
t=k;//给t赋值,以便于后面的使用
}
int add(int val) {
q.push(val);//同样的元素入队
if(q.size()>t)q.pop();//长度大于t时出队
return q.top();
}
private:
int t;
priority_queue<int, vector<int>, greater<int>> q; //
};
/**
* Your KthLargest object will be instantiated and called as such:
* KthLargest* obj = new KthLargest(k, nums);
* int param_1 = obj->add(val);
*/
215. 数组中的第K个最大元素
题目来源
思路
运用优先队列,将数组中的数全存进队列中,队列会自动排序(用大顶堆),然后将前k-1个数从队头踢出后,队头就是第k大的数。
AC代码
class Solution {
public:
int findKthLargest(vector<int>& a, int k) {
priority_queue<int,vector<int>>q;//从大到小
for(auto x:a)
q.push(x);//入队
while(k!=1)
q.pop(),k--;
return q.top();
}
};
例题推荐
牛客14847
P7072 [CSP-J2020] 直播获奖
P2085 最小函数值
SP15376 RMID - Running Median
堆和优先队列紧密联系,优先队列是堆的实现方式之一。
- 下面总结一下什么时候大概会使用:
需要频繁地(在动态插入/删除元素的过程中)获取或移除当前集合中的最大值或最小值,且要求这些操作高效(通常要求 O(log n) 的时间复杂度)。
-
获取动态中位数: 用对顶堆,用一个大顶堆存较小一半数,一个小顶堆存较大一半数。
-
滑动窗口极值:虽然双端队列更优,但优先队列(最大堆/最小堆)也可以更清晰方便的解决。
-
数组中第 K 大/小的元素:维护一个大小为 K 的最小堆(求第 K 大)或最大堆(求第 K 小)。
-
时间复杂度优化:当朴素算法需要频繁遍历查找最小值/最大值时,用优先队列替换可以大幅降低时间复杂度(从 O(n) 降到 O(log n) 每次操作)
总结一句话:当你需要在动态数据集上高效地(O(log n))反复获取或移除最大值或最小值时,优先队列(堆)就是你的首选武器!