堆&优先队列(加强版)

想对优先队列有个初步的了解可以看我的这篇博客:
优先队列
大佬文章
想了解堆的基础推荐看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;
}

解释说明

  1. 变量名定义

    • l(left):大顶堆,存储 较小的一半数字(堆顶是最大值)。
    • r(right):小顶堆,存储 较大的一半数字(堆顶是最小值)。
  2. 插入规则

    • 新数字 ≤ l.top() → 插入 l(左边堆)。
    • 否则插入 r(右边堆)。
  3. 平衡规则

    • l 的大小不能比 r 大超过 1,否则把 l 的堆顶移到 r
    • r 的大小不能超过 l,否则把 r 的堆顶移到 l
  4. 中位数计算

    • 偶数个数字(l.top() + r.top()) / 2
    • 奇数个数字l.top()(因为 l 的大小比 r 大 1)。

运行过程
nums[]:[4, 2, 6, 1, 5]

插入数字堆状态(lr)中位数计算中位数
4[4][]l.top()4
2[2,4][] → 平衡→ [2] [4](2+4)/23
6[2][4,6] → 平衡→ [2,4] [6]44
1[1,2,4][6] → 平衡→ [1,2] [4,6](2+4)/23
5[1,2][4,5,6] → 平衡→ [1,2,4] [5,6]44

最终输出(全整数中位数):

插入 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} A1N

输出格式

⌊ N + 1 2 ⌋ \lfloor \frac{N + 1}2\rfloor 2N+1 行,第 i i i 行为 A 1 … 2 i − 1 A_{1\dots 2i - 1} A12i1 的中位数。

输入 #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 N100

对于 40 % 40\% 40% 的数据, N ≤ 3000 N \le 3000 N3000

对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 100000 1 \le N ≤ 100000 1N100000 0 ≤ A i ≤ 1 0 9 0 \le A_i \le 10^9 0Ai109
思路

很简单的一道模板题,依照上诉介绍,我们可以分别定义一个大顶堆用来存储较小的一半数,和一个小顶堆用来存较大的一半数,因为运用优先队列可自动排序,每第奇数个中位数就是大顶堆的堆顶,因为当二者长度之和等于奇数时,大顶堆的长度一定比小顶堆的长度大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数据库输出
1ADD(3) 0 0 0 3 3 3/
2GET 1 1 1 3 3 3 3 3 3
3ADD(1) 1 1 1 1 , 3 1,3 1,3/
4GET 2 2 2 1 , 3 1,3 1,3 3 3 3
5ADD(-4) 2 2 2 − 4 , 1 , 3 -4,1,3 4,1,3/
6ADD(2) 2 2 2 − 4 , 1 , 2 , 3 -4,1,2,3 4,1,2,3/
7ADD(8) 2 2 2 − 4 , 1 , 2 , 3 , 8 -4,1,2,3,8 4,1,2,3,8/
8ADD(-1000) 2 2 2 − 1000 , − 4 , 1 , 2 , 3 , 8 -1000,-4,1,2,3,8 1000,4,1,2,3,8/
9GET 3 3 3 − 1000 , − 4 , 1 , 2 , 3 , 8 -1000,-4,1,2,3,8 1000,4,1,2,3,8 1 1 1
10GET 4 4 4 − 1000 , − 4 , 1 , 2 , 3 , 8 -1000,-4,1,2,3,8 1000,4,1,2,3,8 2 2 2
11ADD(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 个。现在用两个整数数组来表示命令串:

  1. 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]

  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} 1n,m104
  • 对于 50 % 50\% 50% 的数据, 1 ≤ n , m ≤ 1 0 5 1 \leq n,m \leq 10^{5} 1n,m105
  • 对于 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} 1n,m2×105,ai2×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))反复获取或移除最大值或最小值时,优先队列(堆)就是你的首选武器!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值