295.数据流中的中位数
力扣295/LCR 160. 数据流中的中位数
分析
本题需要动态地寻找中位数,即一边添加元素一边寻找中位数,所以不可能每次找中位数的时候都排个序再找中间元素。
假如把一个有序数组分为两个部分(一部分小于等于中位数,一部分大于等于中位数),通过下图的中位数情况,我们可以发现:
- 两部分元素个数相同,中位数是
蓝色部分最大值
和橙色部分最小值
的平均值 - 两部分元素个数不同且仅相差
1
时,中位数是蓝色部分最大值
或橙色部分最小值
- 中位数值仅与上述元素的位置有关,与其他元素无关
方法:优先队列(堆)
基于以上性质,我们可以用一个大根堆
存储蓝色区域的值
,一个小根堆
存储橙色区域的值
,这样可以让我们只关注堆顶元素。至于其他值,我们只关注其在正确堆中位置即可,如下图所示。求平均数时,根据两个堆的元素个数分情况求取。
堆是一种数据结构,也是一颗完全二叉树。大根堆的每个节点的值都大于等于他的左右子节点的值大,所以堆顶(完全二叉树的根)是整个堆最大的元素,小根堆同理。优先队列是通过堆来实现队头最大/最小的。
核心就在于我们如何得到这两个堆,即如何将不断输入的元素正确分流到两个堆中并保证数量差距不大于1。
-
数量差距不大于1:假如
0 <= 橙色部分元素个数 - 蓝色部分元素个数 <= 1
,- 则当元素个数相等时,需要将某元素分到橙色部分
- 元素个数不等时(橙色多一个),则需要将某元素分到蓝色部分。
-
正确分流到两个堆:上一条内容中,某元素的选取十分关键,我们不能直接把输入的元素不加验证地直接放入,否则将打破这两个堆的整体顺序。而输入元素的值我们无法确定,没法立即知道他应该被放在哪个堆。但是我们可以知道的是,大根堆最大元素可以直接放到小根堆中当最小元素,且不影响整体顺序,小根堆同理。所以假如橙色部分需要增加一个元素,那么我们将输入的元素放到蓝色部分,“锤炼”一番(堆的调整)过后,再将“佼佼者”(堆顶)升入橙色部分;蓝色部分同理,即每次我们都是将一个堆顶移入另一个堆顶。
元素4若放入小根堆,则是堆顶,此时:大根堆 <= 元素4 <= 小根堆,符合整体顺序。
而元素3不能如此,否则:大根堆(有元素4) ? 元素3 <= 小根堆,不符合整体顺序。
代码
时间复杂度:添加数字后堆的调整操作复杂度为O(log n)
。
空间复杂度O(n)
: 其中 n
为数据流中的元素数量,两个堆中最多有n
个元素。
class MedianFinder {
public:
MedianFinder() {
}
priority_queue<int,vector<int>,greater<int>> qmax; // 大根堆
priority_queue<int,vector<int>,less<int>> qmin; // 小根堆
void addNum(int num) {
if(qmax.size() == qmin.size()) { // 元素个数相等,需找个元素放到橙色部分(小根堆)
qmax.push(num); // 先在蓝色部分锤炼一番,再将堆顶弹出
int v = qmax.top();
qmax.pop();
qmin.push(v);
} else { // 元素个数不相等,需找个元素放到蓝色部分(大根堆)
qmin.push(num);
int v = qmin.top();
qmin.pop();
qmax.push(v);
}
}
double findMedian() {
if(qmax.size() == qmin.size())
return (qmax.top() + qmin.top()) / 2.0;
return qmin.top();
}
};