DSAPrep
HardHeap / Priority Queue

Find Median From Data Stream

The median is the middle value in an ordered integer list. If the size of the list is even, there is no single middle value, and the median is the mean of the two middle values.

For example, for arr = [2,3,4], the median is 3. For arr = [2,3], the median is (2 + 3) / 2 = 2.5.

Implement the MedianFinder class: MedianFinder() initializes the object. void addNum(int num) adds the integer num from the data stream to the data structure. double findMedian() returns the median of all elements so far. Answers within 10^-5 of the actual answer are accepted.

Example 1

Input: ["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"], [[],[1],[2],[],[3],[]]
Output: [null,null,null,1.5,null,2.0]
Explanation: addNum(1) -> [1]. addNum(2) -> [1,2]. findMedian() -> (1+2)/2 = 1.5. addNum(3) -> [1,2,3]. findMedian() -> 2.0.

Constraints

  • -10^5 <= num <= 10^5
  • There will be at least one element in the data structure before findMedian is called.
  • At most 5 * 10^4 calls will be made to addNum and findMedian.
Follow-up: If all integers from the stream are in the range [0, 100], how would you optimize your solution? If 99% of integers from the stream are in the range [0, 100], how would you optimize it?
View original on LeetCode β†—

The median needs the middle of a sorted view of everything seen so far, but re-sorting on every call is wasteful. The key design idea: split the stream into two halves β€” a max-heap holding the smaller half and a min-heap holding the larger half β€” and keep them balanced in size. The median is then always derived from the two roots, no sorting required.

Insertion Sort into a Sorted List

Time addNum O(n), findMedian O(1)Space O(n)

Keep a running sorted list. Every addNum does a binary search for the insertion point (O(log n)) but then has to shift elements to make room (O(n)), so insertion is linear overall. findMedian just reads the middle index (or averages the two middle indices).

import bisect
class MedianFinder:
def __init__(self):
self.data = []
def addNum(self, num: int) -> None:
bisect.insort(self.data, num)
def findMedian(self) -> float:
n = len(self.data)
mid = n // 2
if n % 2 == 1:
return float(self.data[mid])
return (self.data[mid - 1] + self.data[mid]) / 2

This is simple and correct, but with up to 5 * 10^4 calls, the O(n) shifting cost per insertion adds up to O(nΒ²) overall in the worst case.

Two Heaps

OptimalTime addNum O(log n), findMedian O(1)Space O(n)

Maintain two heaps that together hold every number seen:

  • small: a max-heap (negated, since Python’s heapq is min-heap only) holding the smaller half of the numbers.
  • large: a min-heap holding the larger half.

Kept balanced so len(small) is always either equal to len(large) or exactly one more. That invariant means the median is either small’s root alone (odd total count) or the average of both roots (even total count) β€” no scan required.

To insert a number while preserving both the balance invariant and the ordering invariant (every element in small is less than or equal to every element in large) in one motion: always push into small first, then immediately move small’s largest into large β€” this guarantees correct ordering even if the new number actually belonged in large. Then, if that left large bigger than small, move large’s smallest back.

import heapq
class MedianFinder:
def __init__(self):
self.small = [] # max-heap (store negatives), holds the smaller half
self.large = [] # min-heap, holds the larger half
def addNum(self, num: int) -> None:
heapq.heappush(self.small, -num)
# move small's max into large, guaranteeing small's max <= large's min
heapq.heappush(self.large, -heapq.heappop(self.small))
if len(self.large) > len(self.small):
heapq.heappush(self.small, -heapq.heappop(self.large))
def findMedian(self) -> float:
if len(self.small) > len(self.large):
return float(-self.small[0])
return (-self.small[0] + self.large[0]) / 2

Tracing the example β€” addNum(1), addNum(2), findMedian(), addNum(3), findMedian() β€” watch the new number always enter small first, a value cross the seam whenever an invariant is threatened, and each median come straight off the two facing tops:

Stream

empty

smallmax-heap

empty

largemin-heap

empty

Shared number line

123

numbers sit on the line at their value β€” indigo chips live in small, slate chips in large, so the smaller half always parks to the left

small.top = -large.top = -sizes: 0 vs 0
1 / 11
currentcomparingresult
invariant: every value in small is <= every value in large, and the heaps differ in size by at most 1

Start: no numbers seen yet. Two heaps will hold everything: small (a max-heap) keeps the smaller half, large (a min-heap) keeps the larger half. Two invariants to protect after every addNum: every value in small <= every value in large, and the heaps differ in size by at most 1 with small never smaller.

Both invariants survive every call, and each median was read directly off the heap tops β€” no scan, no comparisons beyond the top values.

Correctness: the push-then-shuffle sequence in addNum guarantees two invariants after every call: (1) every value in small is <= every value in large, and (2) the two heaps differ in size by at most 1, with small never smaller than large. Together these mean small’s root is always the true lower-median candidate and large’s root the upper-median candidate β€” reading them off directly gives the median without ever sorting.

Complexity: addNum does a constant number of heap pushes/pops, each O(log n). findMedian just reads one or two roots, O(1). Space is O(n) to store every number across both heaps.

Follow-ups: if every value is known to be in [0, 100], a counting array of size 101 tracking frequencies (plus a running total count) lets you walk it to find the middle index in O(100) = O(1) per call instead of O(log n). If 99% of values fall in [0, 100] but a few outliers don’t, keep that counting array for the common range plus a small fallback structure (e.g. a sorted list or the two-heap approach) for the rare out-of-range values, merging the two only when a query needs to cross the boundary.