LeetCode #295 Hard

Find Median from Data Stream

Support addNum and findMedian as numbers stream in, both fast.

heapdesigntwo-heaps
Open on LeetCode ↗
02

Intuition

Keep the lower half in a max-heap and the upper half in a min-heap. The median is at the roots — no scanning. Each insert goes into one heap and rebalances at most one element across, so both roots always face the middle.

How to spot this pattern

Two heaps is the standard answer whenever you need a running order statistic — a median, or the k-th smallest of a stream. The idea is to split the data at the answer: a max-heap holds the lower half so its root is the largest small value, a min-heap holds the upper half so its root is the smallest large value. The median is always sitting at one or both roots, so reading it is O(1).

03

Approach

1

Two heaps face each other

lo (max-heap) holds the smaller half, hi (min-heap) the larger. Invariant: every lo element ≤ every hi element, sizes differ by ≤ 1.

2

Insert then rebalance

Push into lo, move lo's max to hi (guaranteeing the ordering invariant), then if hi outgrew lo, move hi's min back. Two heap ops, done.

3

Median from the roots

Odd count → lo's root (lo is the bigger heap). Even → average of both roots. O(1).

04

Solution & live demo

1import heapq
2 
3class MedianFinder:
4 def __init__(self):
5 self.lo = [] # max-heap (negated)
6 self.hi = [] # min-heap
7 
8 def addNum(self, num):
9 heapq.heappush(self.lo, -num)
10 heapq.heappush(self.hi, -heapq.heappop(self.lo))
11 if len(self.hi) > len(self.lo):
12 heapq.heappush(self.lo, -heapq.heappop(self.hi))
13 
14 def findMedian(self):
15 if len(self.lo) > len(self.hi):
16 return -self.lo[0]
17 return (-self.lo[0] + self.hi[0]) / 2
05

Common pitfalls

Pushing straight onto the heap the value seems to belong to

✗ Wrong
if not self.lo or num <= -self.lo[0]:
    heapq.heappush(self.lo, -num)
else:
    heapq.heappush(self.hi, num)
✓ Right
heapq.heappush(self.lo, -num)
heapq.heappush(self.hi, -heapq.heappop(self.lo))

Both are correct, but the branchy version needs a separate rebalancing block and multiplies the cases you can get wrong. Pushing through lo and immediately relaying its largest element to hi enforces the ordering invariant unconditionally, with no comparison at all.

Forgetting to negate on the way out

✗ Wrong
return self.lo[0]
✓ Right
return -self.lo[0]

heapq is a min-heap only, so a max-heap is simulated by storing negatives. Every value crossing that boundary must flip sign — on the way in, on the way out, and on every transfer between the heaps.

Letting the heaps drift out of balance

✗ Wrong
heapq.heappush(self.lo, -num)
heapq.heappush(self.hi, -heapq.heappop(self.lo))
✓ Right
heapq.heappush(self.lo, -num)
heapq.heappush(self.hi, -heapq.heappop(self.lo))
if len(self.hi) > len(self.lo):
    heapq.heappush(self.lo, -heapq.heappop(self.hi))

Without the third step every insert grows hi and shrinks lo, so the split stops falling at the middle and the roots stop being the median. Sizes must stay equal, or lo ahead by exactly one.

06

Edge cases

Values arriving in sorted order

Rebalancing keeps halves even regardless of arrival order.

Duplicates crossing the median

Invariant is ≤ so equal values may sit on both sides — fine.

07

Complexity

Time
O(log n) add, O(1) median
Space
O(n)
Two heaps, sizes within 1.