图解题库 / 堆 · 对顶堆

295. 数据流的中位数

困难 含交互动画

学完这道你应该能: 说清它为什么用「对顶堆」,而不是死记步骤; 合上页面,自己默写出核心代码; 用 30 秒把思路讲清楚。

题目描述

设计一个结构,支持 addNum(x) 不断加数、findMedian() 随时返回当前所有数的中位数

依次加入 = 5, 3, 8, 1
每次中位数 = 5 → 4 → 5 → 4

思路解析

每问一次就把所有数排序,查得越频繁越崩。我们要的是「随时 O(1) 拿到中位数」。

把数据从中间劈开:左边一半用大顶堆(堆顶 = 这半里最大的),右边一半用小顶堆(堆顶 = 那半里最小的)。中位数就在这两个堆顶上。

每加一个数,先丢进合适的一边,再把两堆调平衡。奇数个时中位数 = 数多的那个堆顶;偶数个时 = 两个堆顶的平均。

加入 5:第一个数 5,放进左边的大顶堆。就一个数,中位数当然是 5。(堆顶在最右端)

加入 3:加入 3,调平衡后变成:左堆装 3、右堆装 5。两个数,中位数取两个堆顶平均 = 4。

加入 8:加入 8(较大,进右边),再平衡。左堆 [3,5]、右堆 [8],奇数个,中位数 = 数多的左堆顶 5。

加入 1:加入 1(较小,进左边),平衡后左堆 [1,3]、右堆 [5,8]。四个数 5,3,8,1 排序是 1,3,5,8,中间两个 3、5,中位数 = 平均 4。

「动态求中位数 / 求第 K 大」的经典结构:一个大顶堆 + 一个小顶堆对顶,维持大小平衡,中位数永远在两个堆顶之间。

参考代码(Python)

Python
1import heapq
2class MedianFinder:
3    def __init__(self):
4        self.small = []   # 大顶堆(存负数模拟),较小一半
5        self.large = []   # 小顶堆,较大一半
6    def addNum(self, x):
7        heapq.heappush(self.small, -heapq.heappushpop(self.large, x))
8        if len(self.small) > len(self.large) + 1:
9            heapq.heappush(self.large, -heapq.heappop(self.small))
10    def findMedian(self):
11        if len(self.small) > len(self.large): return -self.small[0]
12        return (-self.small[0] + self.large[0]) / 2

复杂度分析

  • addNum:O(log n) —— 堆的插入/弹出
  • findMedian:O(1) —— 直接读两个堆顶

套路模板

记住三句话:small 全员 ≤ large 全员、两堆大小差 ≤ 1、奇取多堆顶偶取平均。三条守住就对了。

Python
1small = []  # 大顶堆(负数),装较小一半
2large = []  # 小顶堆,装较大一半
3# 加数:保证 small 的所有数 <= large 的所有数
4# 平衡:两堆大小差 <= 1
5# 中位数:奇数取多的那堆顶,偶数取两堆顶平均

易错点

  • 大顶堆、小顶堆装反(small 装大的) → ✅ small 装较小一半、large 装较大一半(装反了堆顶就不是中间值,中位数全错)
  • 只入堆不平衡 → ✅ 每次 addNum 后检查并调平两堆大小(不平衡的话「中间」会偏到一边,堆顶不再是中位数)
  • Python 直接用 heapq 当大顶堆 → ✅ 存相反数模拟大顶堆(heapq 只有小顶堆,装较小一半要靠存负数来「翻转」)

以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握

下一题 →209. 长度最小的子数组 ← 返回题库