Find Median from Data Stream

Hard Heaps

Problem

Design a data structure that supports adding numbers from a data stream and finding the median of all elements added so far.

  • At most 5 * 10⁴ calls will be made to addNum and findMedian.
  • −10⁵ ≤ num ≤ 10⁵

Example

Input: addNum(1), addNum(2), findMedian(), addNum(3), findMedian()
Output: 1.5, 2

After adding 1 and 2, the data structure contains [1, 2]. The median is (1 + 2) / 2 = 1.5. After adding 3, the data structure contains [1, 2, 3]. The median is 2, the middle element.

Approach

Straightforward Solution

A naive approach is to store all numbers in a list and sort it after each insertion. This leads to O(n log n) time per insertion, which is inefficient for large data streams.

Core Observation

The median divides a sorted list into two halves: the lower half and the upper half. Maintaining these halves separately allows efficient median retrieval. The lower half contains the smaller numbers, and the upper half contains the larger numbers. The median is either the maximum of the lower half, the minimum of the upper half, or the average of both, depending on the total number of elements.

Path to Optimal

The key recognition signals are 'median' and 'data stream', which indicate the need for a data structure that supports efficient insertion and median retrieval. Using two heaps (priority queues) — a max-heap for the lower half and a min-heap for the upper half — allows maintaining the halves in O(log n) time per insertion. Balancing the heaps ensures their sizes differ by at most one, enabling constant-time median calculation.

Optimal Approach

Use two heaps: a max-heap to store the smaller half of the numbers and a min-heap to store the larger half. When adding a number, push it into the max-heap (inverted sign to simulate max-heap). If the largest element in the max-heap is greater than the smallest in the min-heap, swap elements to maintain order. Then rebalance the heaps so their sizes differ by at most one. The median is the top of the larger heap if sizes differ, or the average of the tops if equal.

Time

O(log n) per insertion, O(1) per median retrieval

Each insertion involves pushing into a heap and possibly rebalancing, each operation costing O(log n). Median retrieval is O(1) by peeking at the heap tops.

Space

O(n)

All inserted elements are stored in the two heaps, so space grows linearly with the number of elements.

Pattern Spotlight

Heap / Priority Queue (Dual Heap Median Maintenance)

Maintain two heaps to represent the lower and upper halves of the data stream, balancing their sizes to ensure the median can be retrieved in O(1) time, while insertions remain O(log n).

Solution

1import heapq
2
3class MedianFinder:
4 def __init__(self):
5 self.small = []
6 self.large = []
7
8 def addNum(self, num: int) -> None:
9 heapq.heappush(self.small, -1 * num)
10
11 if (self.small and self.large and
12 (-1 * self.small[0]) > self.large[0]):
13 val = -1 * heapq.heappop(self.small)
14 heapq.heappush(self.large, val)
15
16 if len(self.small) > len(self.large) + 1:
17 val = -1 * heapq.heappop(self.small)
18 heapq.heappush(self.large, val)
19 if len(self.large) > len(self.small) + 1:
20 val = heapq.heappop(self.large)
21 heapq.heappush(self.small, -1 * val)
22
23 def findMedian(self) -> float:
24 if len(self.small) > len(self.large):
25 return -1 * self.small[0]
26 if len(self.large) > len(self.small):
27 return self.large[0]
28
29 return (-1 * self.small[0] + self.large[0]) / 2.0

Step-by-Step Solution

1

Initialize Two Heaps for Median Maintenance

4def __init__(self):
5 self.small = []
6 self.large = []

Objective

To create the lower-half max-heap and upper-half min-heap used to maintain the stream median.

Key Insight

The median is determined by the boundary between the lower and upper halves. Keeping the lower half in a max-heap and the upper half in a min-heap gives constant-time access to the two middle candidates.

Interview Quick-Check

Core Logic

The lower half needs max-heap behavior; Python simulates it with negative values.

Core Logic

The upper half uses a normal min-heap.

2

Insert Number and Maintain Heap Order

8def addNum(self, num: int) -> None:
9 heapq.heappush(self.small, -1 * num)
11 if (self.small and self.large and
12 (-1 * self.small[0]) > self.large[0]):
13 val = -1 * heapq.heappop(self.small)
14 heapq.heappush(self.large, val)

Objective

To insert the new number and restore the invariant that every lower-half value is less than or equal to every upper-half value.

Key Insight

The new number is first added to the lower half. If the largest lower-half value is greater than the smallest upper-half value, that boundary value is moved into the upper half.

Interview Quick-Check

Core Logic

Insert into the lower heap first, then repair ordering if the heap tops cross.

State & Boundaries

Compare only after confirming both heaps contain values.

3

Balance Heap Sizes to Maintain Median Property

16if len(self.small) > len(self.large) + 1:
17 val = -1 * heapq.heappop(self.small)
18 heapq.heappush(self.large, val)
19if len(self.large) > len(self.small) + 1:
20 val = heapq.heappop(self.large)
21 heapq.heappush(self.small, -1 * val)

Objective

To ensure the two heaps differ in size by at most one element.

Key Insight

The median is either the top of the larger heap or the average of both heap tops. That only works if the heap sizes remain balanced.

Interview Quick-Check

Core Logic

Move one top element from the larger heap to the smaller heap when size difference exceeds one.

4

Retrieve Median Based on Heap Sizes

23def findMedian(self) -> float:
24 if len(self.small) > len(self.large):
25 return -1 * self.small[0]
26 if len(self.large) > len(self.small):
27 return self.large[0]
29 return (-1 * self.small[0] + self.large[0]) / 2.0

Objective

To return the correct median from heap tops in constant time.

Key Insight

If one heap is larger, its top is the median. If both heaps are equal size, the median is the average of the lower-half maximum and upper-half minimum.

Interview Quick-Check

Common Pitfalls & Bugs

Remember to negate the lower heap top in Python before using it as a real value.

Line Analysis

This solution has 4 Critical lines interviewers watch for.

Line 9 Critical
heapq.heappush(self.small, -1 * num)

Push the negative of the new number into the max-heap to simulate max-heap behavior.

Python's heapq is a min-heap by default; negating the number allows it to behave as a max-heap, ensuring the largest number in the lower half is accessible at the top.

Line 12 Critical
(-1 * self.small[0]) > self.large[0]):

Compare the lower-half maximum against the upper-half minimum.

If the largest lower-half value exceeds the smallest upper-half value, the two heaps are ordered incorrectly and must be repaired.

Line 25 Critical
return -1 * self.small[0]

Return the median as the negated top element of the max-heap.

Negating the stored negative value retrieves the actual median when the lower half is larger.

Line 29 Critical
return (-1 * self.small[0] + self.large[0]) / 2.0

Return the average of the tops of both heaps when they have equal size.

When both halves are equal in size, the median is the average of the largest element in the lower half and the smallest in the upper half, computed efficiently by accessing the heap tops.

Line 5 Core
self.small = []

Initialize an empty max-heap to store the smaller half of the numbers.

Using a max-heap for the lower half allows quick access to the largest element in the smaller half, which is essential for median calculation and maintaining order between halves.

Line 6 Core
self.large = []

Initialize an empty min-heap to store the larger half of the numbers.

The min-heap stores the larger half, enabling quick access to the smallest element in the upper half, which is critical for balancing and median retrieval.

Line 11 Core
if (self.small and self.large and

Check whether both heaps exist before comparing their boundary values.

This condition starts the order-invariant check only when both heaps have values to compare.

Line 13 Core
val = -1 * heapq.heappop(self.small)

Remove the largest element from the max-heap.

Popping this element prepares it for insertion into the min-heap, restoring the order invariant between halves.

Line 14 Core
heapq.heappush(self.large, val)

Push the extracted value into the min-heap.

Inserting into the min-heap places the element in the upper half, maintaining the correct partitioning of the data stream.

Line 16 Core
if len(self.small) > len(self.large) + 1:

Check if the max-heap is larger than the min-heap by more than one element.

This condition identifies imbalance in heap sizes that would invalidate median calculation, triggering rebalancing.

Line 17 Core
val = -1 * heapq.heappop(self.small)

Extract the largest element from the max-heap for rebalancing.

Removing the top element from the larger heap prepares it for transfer to the smaller heap to restore balance.

Line 18 Core
heapq.heappush(self.large, val)

Insert the extracted element into the min-heap.

Moving the element to the smaller heap restores the size invariant, ensuring median correctness.

Line 19 Core
if len(self.large) > len(self.small) + 1:

Check if the min-heap is larger than the max-heap by more than one element.

This condition detects imbalance in the opposite direction, requiring rebalancing to maintain the median property.

Line 20 Core
val = heapq.heappop(self.large)

Extract the smallest element from the min-heap for rebalancing.

Removing the top element from the larger heap prepares it for transfer to the smaller heap to restore balance.

Line 21 Core
heapq.heappush(self.small, -1 * val)

Insert the negated extracted element into the max-heap.

Negating the value before insertion maintains the max-heap property of the lower half, completing the rebalance.

Line 27 Core
return self.large[0]

Return the median as the top element of the min-heap.

Directly returning the min-heap's top element yields the median when the upper half is larger.

Line 24 Core
if len(self.small) > len(self.large):

Check if the max-heap contains more elements than the min-heap.

If true, the median is the largest element in the lower half, accessible at the top of the max-heap.

Line 26 Core
if len(self.large) > len(self.small):

Check if the min-heap contains more elements than the max-heap.

If true, the median is the smallest element in the upper half, accessible at the top of the min-heap.

Line 4 Standard
def __init__(self):

Define the MedianFinder constructor.

The constructor initializes the two heap structures used throughout the data stream.

Line 8 Standard
def addNum(self, num: int) -> None:

Define the addNum method.

This method inserts a new stream value while preserving median-maintenance invariants.

Line 23 Standard
def findMedian(self) -> float:

Define the findMedian method.

This method reads the maintained heap state and returns the current stream median.

Test Your Understanding

Why are two heaps necessary instead of a single heap or a sorted list?

Two heaps allow splitting the data into lower and upper halves, enabling efficient insertion and median retrieval. A single heap cannot provide both the maximum of the lower half and minimum of the upper half efficiently, and a sorted list requires O(n) insertion time.

Related Problems

Heaps pattern

Don't just read it. Drill it.

Reconstruct Find Median from Data Stream from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Find Median from Data Stream drill

or drill a free problem