LeetCode #295 Hard

Find Median from Data Stream

Find Median from Data Stream is LeetCode 295 (Hard). Design a MedianFinder class:

  • addNum(num) adds an integer from the stream.
  • findMedian() returns the median of every number added so far: the middle value, or the average of the two middle values when the count is even.

Up to 5 · 10⁴ calls mix the two operations, so sorting again for every query is too slow; both operations need to be fast.

Constraints
  • -10⁵ <= num <= 10⁵
  • There will be at least one element in the data structure before calling findMedian.
  • At most 5 * 10⁴ calls will be made to addNum and findMedian.
heapdesigntwo-heaps
Open on LeetCode ↗
02

Intuition

A median depends only on the middle of the sorted order, never on the rest. So split the numbers into two halves and keep just enough order to see the middle, using two heaps:

  • Lower half: a max-heap, so its top is the largest small number.
  • Upper half: a min-heap, so its top is the smallest large number.

Keep every lower value ≤ every upper value, and keep the lower half the same size as the upper half or one bigger. Then the two tops sit right at the middle: the median is the lower top (odd count) or the average of both tops (even count).

How to spot this pattern

Any running median of a stream, or a sliding-window median, is the two-heaps pattern: split the data at the answer and keep each half in a heap whose top faces the middle. The same idea finds the k-th smallest value of a stream with a single heap of size k.

03

Approach

Try it first

Before reading on: after adding 5, 15, 1 and 3, which two numbers decide the median? What is the least order you must keep to find them again after each new number?

1

Set up two heaps

lo is a max-heap for the smaller half, hi a min-heap for the larger half. Python's heapq only builds min-heaps, so lo stores negated values and flips the sign back on the way out. C++ and Java use a reversed comparator.

2

Push the new number through lo

Push num into lo, then pop the largest value of lo and push it into hi. Whatever num was, the biggest value of the lower half ends up in the upper half, so every value in lo stays ≤ every value in hi without a single comparison.

3

Rebalance the sizes

The previous step always makes hi one bigger. If hi now has more values than lo, pop its smallest value and push it back into lo. This keeps lo the same size as hi or exactly one bigger, so the median's position never moves.

4

Read the median from the tops

If lo is bigger, the count is odd and the median is the top of lo. Otherwise average the two tops as a decimal, (lo top + hi top) / 2.0. In C++ and Java, dividing by the integer 2 would turn 1.5 into 1.

04

Find Median from Data Stream solution in Python | C++ | Java

▶1import heapq
▶2 
▶3 
▶4class MedianFinder:
▶5 def __init__(self):
▶6 self.lo = []
▶7 self.hi = []
▶8 
▶9 def addNum(self, num: int) -> None:
▶10 heapq.heappush(self.lo, -num)
▶11 heapq.heappush(self.hi, -heapq.heappop(self.lo))
▶12 if len(self.hi) > len(self.lo):
▶13 heapq.heappush(self.lo, -heapq.heappop(self.hi))
▶14 
▶15 def findMedian(self) -> float:
▶16 if len(self.lo) > len(self.hi):
▶17 return -self.lo[0]
▶18 return (-self.lo[0] + self.hi[0]) / 2
addinglower halfmax-heapemptyupper halfmin-heapempty0lower size0upper size–medianboth halves empty
stream[5, 15, 1, 3]
lo, hiemptylower and upper half
Start. The lower half lives in a max-heap, so its largest value sits on top; the upper half lives in a min-heap, so its smallest value sits on top. Those two tops are the middle of the sorted stream, which is all a median needs.
adding5lower halfmax-heap5upper halfmin-heapempty1lower size0upper size–medianpush 5 into the lower half
num5
lo[5]largest first
Every new number goes into the lower half first. It may not belong there, and the next line fixes that without any comparison.
adding5lower halfmax-heapemptyupper halfmin-heap5max 5 moves0lower size1upper size–medianmove the lower top 5 to the upper half
moved5largest of the lower half
lo size0
hi size1
Pop the largest value of the lower half, 5, and push it into the upper half. It was the only value below, so it moves up for now. Now every lower value is ≤ every upper value.
adding5lower halfmax-heap5upper halfmin-heapemptymin 5 moves1lower size0upper size–medianupper half too big: move 5 back down
moved back5smallest of the upper half
lo size1
hi size0
The upper half had more values than the lower half. Move its smallest, 5, back down. That keeps the order rule and leaves the lower half one bigger than the upper half.
addinglower halfmax-heap5upper halfmin-heapempty1lower size0upper size5medianodd count: median = lower top 5
findMedian()5
medians so far[5]
Only one number so far, so it is the median: 5. Only the top is read, O(1).
adding15lower halfmax-heap155upper halfmin-heapempty2lower size0upper size–medianpush 15 into the lower half
num15
lo[15, 5]largest first
Every new number goes into the lower half first. It may not belong there, and the next line fixes that without any comparison. It is the largest value in the lower half, so the heap lifts it to the top.
adding15lower halfmax-heap5upper halfmin-heap15max 15 moves1lower size1upper size–medianmove the lower top 15 to the upper half
moved15largest of the lower half
lo size1
hi size1
Pop the largest value of the lower half, 15, and push it into the upper half. That is the number just added: it is the largest value below, so it belongs in the upper half. Now every lower value is ≤ every upper value.
adding15lower halfmax-heap5upper halfmin-heap151lower size1upper size–mediansizes 1 and 1: already balanced
lo size1
hi size1not bigger than lo
The upper half is not bigger than the lower half, so nothing moves back. Sizes are equal now, which means the count is even.
addinglower halfmax-heap5upper halfmin-heap151lower size1upper size10medianeven count: (5 + 15) / 2 = 10
findMedian()10
medians so far[5, 10]
Both halves hold 1 value, so the middle falls between the two tops. Average them as a decimal: 10.
adding1lower halfmax-heap51upper halfmin-heap152lower size1upper size–medianpush 1 into the lower half
num1
lo[5, 1]largest first
Every new number goes into the lower half first. It may not belong there, and the next line fixes that without any comparison.
adding1lower halfmax-heap1upper halfmin-heap515max 5 moves1lower size2upper size–medianmove the lower top 5 to the upper half
moved5largest of the lower half
lo size1
hi size2
Pop the largest value of the lower half, 5, and push it into the upper half. The new number stays below because 5 is bigger than it. Now every lower value is ≤ every upper value.
adding1lower halfmax-heap51upper halfmin-heap15min 5 moves2lower size1upper size–medianupper half too big: move 5 back down
moved back5smallest of the upper half
lo size2
hi size1
The upper half had more values than the lower half. Move its smallest, 5, back down. That keeps the order rule and leaves the lower half one bigger than the upper half.
addinglower halfmax-heap51upper halfmin-heap152lower size1upper size5medianodd count: median = lower top 5
findMedian()5
medians so far[5, 10, 5]
The lower half holds one extra value, so its top is the exact middle of all 3 numbers: 5. Only the top is read, O(1).
adding3lower halfmax-heap513upper halfmin-heap153lower size1upper size–medianpush 3 into the lower half
num3
lo[5, 3, 1]largest first
Every new number goes into the lower half first. It may not belong there, and the next line fixes that without any comparison.
adding3lower halfmax-heap31upper halfmin-heap515max 5 moves2lower size2upper size–medianmove the lower top 5 to the upper half
moved5largest of the lower half
lo size2
hi size2
Pop the largest value of the lower half, 5, and push it into the upper half. The new number stays below because 5 is bigger than it. Now every lower value is ≤ every upper value.
adding3lower halfmax-heap31upper halfmin-heap5152lower size2upper size–mediansizes 2 and 2: already balanced
lo size2
hi size2not bigger than lo
The upper half is not bigger than the lower half, so nothing moves back. Sizes are equal now, which means the count is even.
addinglower halfmax-heap31upper halfmin-heap5152lower size2upper size4medianeven count: (3 + 5) / 2 = 4
findMedian()4
medians so far[5, 10, 5, 4]
Both halves hold 2 values, so the middle falls between the two tops. Average them as a decimal: 4.
05

Common pitfalls

Putting the lower half in a min-heap

✗ Wrong
heapq.heappush(self.lo, num)
✓ Right
heapq.heappush(self.lo, -num)

The lower half must expose its largest value. A plain heapq list exposes the smallest, so its top is the smallest number seen, not the middle. Python stores negatives to fake a max-heap, and every value must flip sign again when it leaves lo.

Skipping the rebalance

✗ 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))

Every insert moves one value into hi, so without the last step lo never keeps anything and stays empty. The first findMedian then reads the top of an empty heap and crashes.

Integer division on an even count

✗ Wrong
return (lo.top() + hi.top()) / 2;
✓ Right
return (lo.top() + hi.top()) / 2.0;

In C++ and Java, int / int drops the fraction, so the median of 1 and 2 comes back as 1 instead of 1.5. Dividing by 2.0 keeps the decimal.

06

Complexity

Time
O(log n) add, O(1) median
Space
O(n)
addNum does at most three heap pushes or pops; findMedian only reads the tops. Every number is stored. Sorting on each query would cost O(n log n) per call.
07

Find Median from Data Stream FAQ

What if every number is between 0 and 100?

Keep a count array of 101 slots instead of heaps. addNum is one increment, O(1). findMedian walks the counts until it reaches the middle position, at most 101 steps, which is O(1) for a fixed range.

What if 99% of the numbers are between 0 and 100?

Use the count array for values in range, plus two counters (or small sorted lists) for values below 0 and above 100. The median almost always lands inside the range, so the walk over the counts still finds it; the rare outliers only shift which position you look for.

Why not insert into a sorted list with binary search?

Finding the position is O(log n), but inserting into the middle of an array shifts up to n values, so each addNum is O(n); this is the insertion-sort approach, O(n²) over the whole stream. A balanced sorted set (C++ multiset, Java TreeMap) with a pointer to the middle also gets O(log n) per insert, but moving that pointer correctly is fiddly. Two heaps give the same O(log n) with simpler code.