Median from a Data Stream
Keep the running median of numbers arriving one at a time, with O(log n) inserts and O(1) median reads. The trick is two heaps balancing each other at the middle — a staple of streaming-statistics interviews.
Prerequisites: Heaps and Priority Queues, Big-O Complexity
Numbers arrive one after another and, at any moment, you want the median — the middle value if you sorted everything seen so far. The lazy solution is to keep a list, sort it on every query, and read the middle: that's per query and hopeless on a fast stream. The elegant solution keeps the data half-sorted at all times using two heaps, so each new number costs and the median is always sitting right there for reading.
The two-heap idea
Split the numbers into a lower half and an upper half. Store the lower half in a max-heap (so its largest element — the top of the low half — is instantly available) and the upper half in a min-heap (so its smallest element — the bottom of the high half — is instantly available). Keep the two halves the same size, give or take one. Then the median is either the top of the bigger heap, or the average of the two tops when the halves are equal.
In words: the max-heap holds everything below the middle and hands you the largest of those; the min-heap holds everything above and hands you the smallest of those. The median lives exactly at that boundary, so you never look deeper than the two tops.
Worked example: streaming 5, 15, 1, 3
Feed the numbers in and watch the heaps rebalance. lo is the max-heap (lower half), hi is the min-heap (upper half):
| Insert | lo (max-heap) | hi (min-heap) | Median |
|---|---|---|---|
| 5 | {5} | {} | 5 |
| 15 | {5} | {15} | (5+15)/2 = 10 |
| 1 | {1, 5} | {15} | 5 |
| 3 | {1, 3} | {5, 15} | (3+5)/2 = 4 |
Each insert: push onto one heap, then, if one side grew too big or a value landed on the wrong side, move a single element across the boundary to rebalance. That's a constant number of heap operations, each .
import heapq
class MedianFinder:
def __init__(self):
self.lo = [] # max-heap (store negatives — Python heaps are min-heaps)
self.hi = [] # min-heap
def add(self, x):
heapq.heappush(self.lo, -x) # tentatively low
heapq.heappush(self.hi, -heapq.heappop(self.lo)) # largest low -> high
if len(self.hi) > len(self.lo): # keep lo the bigger side
heapq.heappush(self.lo, -heapq.heappop(self.hi))
def median(self):
if len(self.lo) > len(self.hi):
return -self.lo[0]
return (-self.lo[0] + self.hi[0]) / 2
The push-to-lo-then-shift-to-hi dance guarantees the two heaps stay ordered relative to each other and balanced in size, without any explicit comparison of the incoming value — a clean trick worth recognizing.
Two heaps facing each other at the middle: a max-heap for the lower half, a min-heap for the upper half, sizes kept within one. Inserts are ; the median is always the top of the larger heap (or the average of the two tops), read in .
Python's heapq only builds min-heaps, so simulate a max-heap by pushing negated values and negating again when you read them back. Forgetting one of those two negations is the single most common bug in this problem.
Where it shows up in quant-dev interviews
This is a top-tier streaming question because desks compute rolling statistics on tick data constantly — a running median or a rolling quantile of trade sizes, spreads, or returns, updated on every message without re-sorting the window. The interviewer's follow-ups usually push toward a sliding window (evict the oldest value as it leaves the window, which needs lazy deletion or an indexed structure) or toward an arbitrary percentile rather than the exact middle. The same two-heap balancing idea underpins order-statistic queries generally (Order Statistics) and connects to quantile estimation on streams (Quantile Regression).
Rebalance after every insert, not lazily. If the heaps drift more than one element apart in size, or a value ends up in the wrong half, the tops no longer straddle the true median and every subsequent answer is quietly wrong. Test the even/odd count transition explicitly — that boundary is where off-by-one errors hide.
If you can explain why the two tops bracket the median, keep the sizes balanced, and handle the max-heap-via-negation detail, you've got one of the most reusable streaming patterns there is — a close cousin of the -access thinking behind an LRU Cache Design.
Practice in interviews
Further reading
- Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms (Heaps)
- Sedgewick & Wayne, Algorithms, 4th ed. (Priority Queues)