LeetCode 295. 数据流的中位数
本节目标
用一大一小两个堆维护数据的左右两半,在在线插入后以常数时间读取中位数。
这道题是数据结构综合应用框架中的拓展题。单个堆擅长维护一侧的极值,而中位数位于整个有序序列的中间,需要两个堆共同维护数据的左右两半。
题意与约束
设计一个数据结构,支持:
addNum(num):向数据流加入一个整数;findMedian():返回当前所有元素的中位数。
元素个数为奇数时,中位数是排序后正中间的数;元素个数为偶数时,中位数是中间两个数的平均值。
每次查询时重新排序全部历史数据会重复大量工作。我们希望让一次插入只进行局部调整,并随时从数据结构的边界读取中位数。
两个堆分别维护左右两半
把有序数据想象成左右两部分:
lower保存较小的一半,使用大根堆,因此堆顶是左半部分最大值;upper保存较大的一半,使用小根堆,因此堆顶是右半部分最小值。
只要两个堆满足正确的不变量,中位数就在两个堆顶:
- 总数为奇数时,让
lower比upper多一个元素,中位数是lower堆顶; - 总数为偶数时,两堆大小相等,中位数是两个堆顶的平均值。
必须同时维护两个不变量
顺序不变量
lower 中任意元素都不大于 upper 中任意元素。
加入新数时,若它不大于 lower 堆顶,就先进入 lower;否则进入 upper。这样新元素先落在正确的一侧。
大小不变量
始终满足:
lower.size == upper.size
或
lower.size == upper.size + 1
插入后若某一侧元素过多,就把该侧堆顶移动到另一侧。移动的是最靠近中间边界的元素,因此调整大小的同时不会破坏顺序。
只有顺序正确但数量失衡,中间位置可能不在堆顶;只有数量平衡但左右混杂,两个堆顶也不能代表中间值。这两个不变量缺一不可。
示例推演
依次加入 5、2、8、3:
| 新元素 | lower 保存的较小一半 | upper 保存的较大一半 | 中位数 |
|---|---|---|---|
| 5 | {5} | {} | 5 |
| 2 | {2} | {5} | 3.5 |
| 8 | {5, 2} | {8} | 5 |
| 3 | {3, 2} | {5, 8} | 4 |
表中只展示集合内容;真正需要立即访问的是 lower 的最大值与 upper 的最小值。
代码实现
C++ 用一个默认大根堆和一个 greater<int> 小根堆;Python 用负数模拟大根堆。两份实现都先选择插入侧,再通过移动堆顶恢复大小不变量。
- C++
- Python
#include <functional>
#include <queue>
#include <vector>
using namespace std;
class MedianFinder {
private:
priority_queue<int> lower;
priority_queue<int, vector<int>, greater<int>> upper;
public:
MedianFinder() = default;
void addNum(int num) {
if (lower.empty() || num <= lower.top()) {
lower.push(num);
} else {
upper.push(num);
}
if (lower.size() > upper.size() + 1) {
upper.push(lower.top());
lower.pop();
} else if (upper.size() > lower.size()) {
lower.push(upper.top());
upper.pop();
}
}
double findMedian() {
if (lower.size() > upper.size()) {
return lower.top();
}
return lower.top() / 2.0 + upper.top() / 2.0;
}
};
import heapq
class MedianFinder:
def __init__(self):
self.lower = []
self.upper = []
def addNum(self, num):
if not self.lower or num <= -self.lower[0]:
heapq.heappush(self.lower, -num)
else:
heapq.heappush(self.upper, num)
if len(self.lower) > len(self.upper) + 1:
heapq.heappush(self.upper, -heapq.heappop(self.lower))
elif len(self.upper) > len(self.lower):
heapq.heappush(self.lower, -heapq.heappop(self.upper))
def findMedian(self):
if len(self.lower) > len(self.upper):
return float(-self.lower[0])
return (-self.lower[0] + self.upper[0]) / 2.0
C++ 在计算偶数个元素的中位数时分别执行 lower.top() / 2.0 与 upper.top() / 2.0 后再相加,避免先做两个整数的加法可能溢出。
复杂度分析
addNum时间复杂度:O(log n)。插入一次,至多再移动一个堆顶。findMedian时间复杂度:O(1)。答案直接来自一个或两个堆顶。- 空间复杂度:
O(n)。为支持未来更新,需要保留数据流中的全部元素,只是分布在两个堆中。
易错点
- 只维护两个堆大小接近,却没有保证左侧元素都不大于右侧元素。
- 调整顺序后忘记恢复大小,导致奇数个元素时中位数不在约定的堆顶。
- Python 忘记把大根堆中的负数还原为正数。
- 偶数个元素时使用整数除法,丢失
.5。 - C++ 先计算
lower.top() + upper.top(),极端整数可能在转换为浮点数之前溢出。
模式迁移
双堆把“持续维护 Top K”扩展成“持续维护分界线两侧”。当查询目标从中位数变为某个固定百分位数时,可以调整两个堆的目标大小比例;当窗口会删除旧元素时,还需要额外记录延迟删除信息。后两类属于更复杂的维护问题,本题先掌握顺序与大小两个基本不变量即可。