题意
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如:
[2,3,4] 的中位数是 3;
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
- void addNum(int num) :从数据流中添加一个整数到数据结构中。
- double findMedian() :返回目前所有元素的中位数。
解题思路
题目要求获取一个数据流排序后的中位数,那么可以使用两个优先队列(堆)实现。
题使用一个大顶堆,一个小顶堆完成。
- 大顶堆的每个节点的值大于等于左右孩子节点的值,堆顶为最大值。
- 小顶堆的每个节点的值小于等于左右孩子节点的值,堆顶为最小值。
因此我们使用 大顶堆(maxHeap) 来存储数据流中较小一半的值,用 小顶堆(minHeap) 来存储数据流中较大一半的值。即“大顶堆的堆顶”与“小顶堆的堆顶”就是排序数据流的两个中位数
。
如图所示,大顶堆(maxHeap)置于下方,小顶堆(minHeap)倒置于上方,两个堆组合起来像一个沙漏的形状。
根据堆的性质,且大顶堆存放的是较小一半的数值,小顶堆存放较大一半的数值,可以判断两个堆的值从下往上递增,即:
maxHeap堆底 <= maxHeap堆顶 <= minHeap堆顶 <= minHeap堆底。
题目要求获取数据流排序后的中位数,而根据数据流的奇偶性以及堆的性质,将获取中位数的情况分为两类:
- 数据流为奇数时,保证两个堆的长度相差1,那么长度较大的堆的堆顶就是中位数;
- 数据流为偶数时,保证两个堆的长度相等,两个堆的堆顶相加除二就是中位数。
那么我们要保证每次插入元素后,两个堆要 维持相对长度。让minHeap为长度较大的堆,每次插入元素时进行判断:
- 当两堆总长度为偶数时,即两堆长度相等,将新元素插入到minHeap,插入后minHeap比maxHeap长度大1;
- 当两堆总长度为奇数时,即两堆长度不等,将新元素插入到maxHeap,插入后两堆长度相等;
还要保证插入元素后,两堆仍是保证从下往上递增的顺序性。如上面的偶数情况,将新元素x直接插入到minHeap,是有可能破坏两堆的顺序性的,例如:(minHeap是存储“较大一半”的值)
- 若x的值恰好为“较大一半”,直接将插入到“较大一半”的minHeap中,是不会破坏顺序的;
- 若x的值为“较小一半”,而此时却插入到“较大一半”的minHeap中,是会破坏顺序的。
那么,每次新元素插入时,都需要先插入到另一个堆,进行重新排序后,再将最值拿出来插入正确的堆中。因此,最终得出的结论为:
- 当两堆总大小为偶数时,即两堆大小相等,先将新元素插入maxHeap,重新排序后将新的最值拿出并插入到minHeap(这样可以保证插入到minHeap的值是较大一半的值)
- 当两堆总大小为奇数时,即两堆大小不等,先将新元素插入minHeap,重新排序后将新的最值拿出并插入到maxHeap(这样可以保证插入到maxHeap的值是较小一半的值)
讲讲C++的priority_queue
priority_queue 优先级队列,只能“从一端进(称为队尾),从另一端出(称为队头)”,且每次只能访问 priority_queue 中位于队头的元素。但是 先进队列的元素并不一定先出队列,而是优先级最大的元素最先出队列。
每个 priority_queue 容器适配器在创建时,都制定了一种排序规则。根据此规则,该容器适配器中存储的元素就有了优先级高低之分。
举个例子,假设当前有一个 priority_queue 容器适配器,其制定的排序规则是按照元素值从大到小进行排序。根据此规则,自然是 priority_queue 中值最大的元素的优先级最高。
STL中,priority_queue容器适配器定义如下:
template <typename T,typename Container=std::vector<T>,typename Compare=std::less<T> >
class priority_queue
{
//......
}
- typename T:指定存储元素的具体类型;
- typename Compare:指定容器中评定元素优先级所遵循的排序规则,默认使用
std::less<T>
按照元素值从大到小进行排序(即默认是大顶堆),还可以使用std::greater<T>
按照元素值从小到大排序(小顶堆),但更多情况下是使用自定义的排序规则。 - typename Container:指定 priority_queue 底层使用的基础容器,默认使用 vector 容器。
- 作为 priority_queue 容器适配器的底层容器,其必须包含 empty()、size()、front()、push_back()、pop_back() 这几个成员函数, STL序列式容器中只有 vector 和 deque 容器符合条件。
自定义排序
无论 priority_queue 中存储的是基础数据类型(int、double 等),还是 string 类对象或者自定义的类对象,都可以使用函数对象的方式自定义排序规则。例如:
#include<iostream>
#include<queue>
using namespace std;
//函数对象类
template <typename T>
class cmp
{
public:
//重载 () 运算符
bool operator()(T a, T b)
{
return a > b;
}
};
int main()
{
int a[] = { 4,2,3,5,6 };
priority_queue<int,vector<int>,cmp<int> > pq(a,a+5);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
return 0;
}
C++实现
class MedianFinder
{
private:
priority_queue<int,vector<int>,less<int>> max_heap; //大顶堆,存放较小一半的数值
priority_queue<int,vector<int>,greater<int>> min_heap ;//小顶堆,存放较大一半的数值
public:
/** initialize your data structure here. */
MedianFinder()
{
}
void addNum(int num)
{
//如果 大顶堆和小顶堆的数值相等,就先将数值插到大顶堆里面,再从大顶堆的顶端拿出元素插到小顶堆里面
if(max_heap.size()==min_heap.size())
{
max_heap.push(num);
min_heap.push(max_heap.top());
max_heap.pop();
}
else
{
min_heap.push(num);
max_heap.push(min_heap.top());
min_heap.pop();
}
}
double findMedian()
{
if(max_heap.size()==min_heap.size())
{
return (max_heap.top()+min_heap.top()) / (double)2;
}
else
{
return min_heap.top();
}
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/