【算法分析】
本题可以利用对顶堆来求解。对顶堆由一个大根堆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; //小根堆
其中的 priority_queue 中文意思为“优先队列”。priority_queue的基本操作与队列相同。不过需要特别提醒的是, priority_queue 的 push() 函数的作用是将元素插到队尾,并排序。若是大根堆,在队尾插入某个元素后,要与大根堆中已有元素按从大到小排序;若是小根堆,在队尾插入某个元素后,要与小根堆中已有元素按从小到大排序。
对顶堆常用于求解“第K小的元素”及“第K大的元素”问题。因为优先队列priority_queue虽然不像数组那样支持随机访问,但它可以用O(1)的时间查询出堆顶元素。显然,利用对顶堆查询堆顶元素的时间复杂度与数组的随机访问的时间复杂度O(1)持平。特别是当遇到多次动态查询问题时,对顶堆就更高效。此外,对顶堆还可用于解决其他“第K小的元素”及“第K大的元素”问题的变形问题,如求前n个元素的中位数等问题。
求解“第K小的元素”问题的解题思路为:若求第K小的元素,先利用输入序列提供的前K个元素构建一个包括K个元素的大根堆G,后来的元素x先与大根堆G的堆顶元素G.top()比较,如果x>G.top(),则将x加入小根堆L;否则,将大根堆G的堆顶元素G.top()弹出后,将x加入大根堆G。当然,简洁设计方案是先利用输入序列提供的前K个元素构建一个包括K个元素的大根堆G,便可求解“第K小的元素”问题。
求解“第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
int main() {
int k;
cin>>k;
vector<int> v;
int x;
while(cin>>x) {
v.push_back(x);
}
for(int i=0; i<v.size(); i++) {
if(L.size()<k) {
L.push(v[i]);
} else if (v[i]>L.top()) {
L.pop();
L.push(v[i]);
}
}
cout<<L.top()<<" ";
return 0;
}
/*
in:
3
3 2 7 5 8 1
out:
5
*/