295. 数据流的中位数
LeetCode 2951 · 困难 · 堆
题
设计一个类 MedianFinder:addNum(num) 往集合里加一个整数,findMedian() 返回当前所有数的中位数(偶数个时取中间两数的平均)。
例:addNum(1), addNum(2), findMedian() → 1.5, addNum(3), findMedian() → 2.0。
一句话
把数分成两半:较小的一半放大顶堆 lo,较大的一半放小顶堆 hi,保持 len(lo) 等于 len(hi) 或多 1;中位数就是 lo 的堆顶,或两个堆顶的平均。
关键技巧
中位数只关心「分界线」。 不需要全序,只要知道较小一半的最大值和较大一半的最小值——这正是两个堆各自的堆顶。
两个不变量:
- 有序:
lo里每个数 \(\le\)hi里每个数,即 \(\max(lo) \le \min(hi)\); - 平衡:\(|lo| - |hi| \in \{0, 1\}\)。
插入时先过一道对面的堆,再平衡。 新数 x 先推进 lo,把 lo 的最大值弹出推进 hi——这一步保证了有序(进入 hi 的是 lo ∪ {x} 里最大的);然后若 hi 比 lo 大,把 hi 的最小值挪回 lo,恢复平衡。挪回的是 hi 的最小值,有序依然成立。
Python 只有小顶堆,大顶堆用存相反数实现。
解
class MedianFinder:
def __init__(self):
self.lo = [] # 大顶堆(存相反数),较小的一半
self.hi = [] # 小顶堆,较大的一半
def addNum(self, num: int) -> None:
heapq.heappush(self.hi, -heapq.heappushpop(self.lo, -num)) # 过一遍 lo,最大者进 hi
if len(self.hi) > len(self.lo): # 平衡:lo 不少于 hi
heapq.heappush(self.lo, -heapq.heappop(self.hi))
def findMedian(self) -> float:
if len(self.lo) > len(self.hi):
return float(-self.lo[0])
return (-self.lo[0] + self.hi[0]) / 2
addNum \(O(\log n)\),findMedian \(O(1)\),空间 \(O(n)\)。
延伸
- 值域很小(如都在 \([0, 100]\))时用计数数组,找中位数扫 101 个桶即可;99% 在范围内时,范围外的两端各记一个计数,中位数几乎总落在桶里。
- 滑动窗口中位数(LeetCode 480)要支持删除,用对顶堆 + 延迟删除,或有序容器。
- 单堆找第 k 大的基本招见 215. 数组中的第K个最大元素;两个有序数组的中位数(静态、要求对数时间)是另一种思路,见 4. 寻找两个正序数组的中位数。
自测
m = MedianFinder()
m.addNum(1)
m.addNum(2)
assert m.findMedian() == 1.5
m.addNum(3)
assert m.findMedian() == 2.0
m = MedianFinder()
m.addNum(-1)
assert m.findMedian() == -1.0
for x in (-2, -3, -4, -5):
m.addNum(x)
assert m.findMedian() == -3.0
random.seed(0)
m, seen = MedianFinder(), []
for _ in range(500):
x = random.randint(-50, 50)
m.addNum(x)
bisect.insort(seen, x)
n = len(seen)
want = seen[n // 2] if n % 2 else (seen[n // 2 - 1] + seen[n // 2]) / 2
assert m.findMedian() == want
-
LeetCode 295,2026-09-29 读。 ↩