对顶堆简介 → 第K大问题 + topK问题
【对顶堆】
对顶堆由一个大根堆G和一个小根堆L构成,且常以小根堆L的堆顶元素L.top()作为对顶堆的分界点,并满足G.top()<L.top()。 换句话表述,就是“在对顶堆中,大根堆的元素都小于L.top(),小根堆的元素都大于L.top()”。显然,若输入的元素大于L.top(),就加入小根堆。反之,就加入大根堆。 对顶堆常见的示意图如右所示。
由于堆常借助于STL中的 priority_queue<int> 来实现,那么对顶堆便可利用 priority_queue<int> 同时声明一个大根堆G及一个小根堆L来实现。对顶堆代码如下所示:
priority_queue<int> G; //大根堆
priority_queue<int,vector<int>,greater<int> > L; //小根堆
【对顶堆 → 第K大问题】
对顶堆常用于求解“第K小元素”及“第K大元素”问题。因为优先队列priority_queue虽然不像数组那样支持随机访问,但它可以用O(1)的时间查询出堆顶元素。显然,利用对顶堆查询堆顶元素的时间复杂度与数组的随机访问的时间复杂度O(1)持平。特别是当遇到多次动态查询问题时,对顶堆就更高效。此外,对顶堆还可用于解决其他“第K小元素”及“第K大元素”问题的变形问题,如求前n个元素的中位数等问题。
求解“第 K 大元素”问题的解题思路为:若求第 K 大的元素,先利用输入序列提供的前 K 个元素构建一个包括 K 个元素的小根堆 L,后来的元素 x 先与小根堆 L 的堆顶元素 L.top() 比较,如果 x<L.top(),则将 x 加入大根堆 G;否则,将小根堆 L 的堆顶元素 L.top() 弹出后,将 x 加入小根堆 L。当然,简洁设计方案是利用输入序列提供的前 K 个元素构建一个包括 K 个元素的小根堆 L,便可求解“第 K 大元素”问题。
“第 K 大元素”问题的代码如下所示。
#include<bits/stdc++.h>
using namespace std;
priority_queue<int,vector<int>,greater<int> > L; //Small Root Heap
priority_queue<int> G; //Big Root Heap
int main() {
int n,k,x;
cin>>n>>k;
while(n--) {
cin>>x;
if(L.size()<k) L.push(x);
else if(x>L.top()) {
G.push(L.top()); //Optional
L.pop();
L.push(x);
}
}
cout<<L.top()<<endl;
return 0;
}
【topK问题】
“topK 问题”指从包含 n 个元素的数据集中高效获取前 K 个最大或最小元素的算法问题,常见于排行榜、实时数据筛选等场景(如百万用户中筛选前100名活跃用户)。topK问题可以利用对顶堆来求解。
“topK 问题”问题的代码如下所示。
#include<bits/stdc++.h>
using namespace std;
priority_queue<int,vector<int>,greater<int> > L; //Small Root Heap
priority_queue<int> G; //Big Root Heap
int main() {
int n,k,x;
cin>>n>>k;
while(n--) {
cin>>x;
if(L.size()<k) L.push(x);
else if(x>L.top()) {
G.push(L.top()); //Optional
L.pop();
L.push(x);
}
}
while(!L.empty()) {
cout<<L.top()<<" ";
L.pop();
}
return 0;
}
显见,“topK”问题的代码与“第K大元素”问题的代码差别极小。
只需将语句 cout<<L.top()<<" "; 修改为如下语句即可。
while(!L.empty()) {
cout<<L.top()<<" ";
L.pop();
}