Skip to content

Coding patternsBlind 75

Find Median from Data Stream

Split the numbers into a smaller half and a larger half, each in a heap, so the median always sits on the two tops. Solved two ways, a sorted list in O(n) per add and two heaps in O(log n) per add, with a step-through, code in five languages and the Big O worked out from the code.

Numbers arrive one at a time. Build a class MedianFinder with two methods:

  • addNum(num) takes in the next number.
  • findMedian() returns the median of all numbers so far.

The median is the middle value once the numbers are sorted. With an even count there are two middle values, and the median is their average.

Each example below is a list of calls on one new MedianFinder. Only findMedian returns something.

Example 1

Input: addNum(5), addNum(2), findMedian(), addNum(8), findMedian(), addNum(1), addNum(7), findMedian()

Output: 3.5, 5.0, 5.0

Sorted, the numbers are 2, 5, then 2, 5, 8, then 1, 2, 5, 7, 8. The picture shows the two halves at the end, with 5 on top of the smaller half.

top
low521
high78

Example 2

Input: addNum(4), findMedian()

Output: 4.0

One number is its own median.

Example 3

Input: addNum(-1), addNum(-3), findMedian()

Output: -2.0

Two numbers: the average of −3 and −1 is −2.

  • -100,000 ≤ num ≤ 100,000
  • findMedian is only called after at least one number was added.
  • At most 50,000 calls in total.
Does the median have to be exact?
Close enough is fine: answers within 0.00001 are accepted. Averaging two ints as a float is exact enough.
Can numbers repeat?
Yes. Duplicates count separately, like in any sorted list.
Which call happens more?
Either can. So both should be fast, not one fast and one slow.
Can the numbers be negative?
Yes, down to −100,000. Nothing in the method depends on the sign.

The median only needs the middle of the sorted order, not the whole order. If the numbers are split into a smaller half and a larger half, the median is the largest of the small half, the smallest of the large half, or their average. “Keep the largest” and “keep the smallest” are what a heap does fast.

This problem has two approaches. Sorting on every findMedian is slower than the sorted list, so it is not a useful middle step.

Two heaps O(log n) per add

Keep two heaps. A heap is a tree kept in a list that always has its largest (or smallest) item on top, and it can add or remove an item in about log n steps.

  • low is a max-heap with the smaller half of the numbers. Its top is the largest of them.
  • high is a min-heap with the larger half. Its top is the smallest of them.

Two rules hold after every add. Every number in low is at most every number in high. And low has the same size as high, or one more.

To add a number, push it into low, then move the top of low into high. That sends the largest of the small numbers across, so the order rule holds. If high is now bigger, move its top back to low.

Then the median is on the tops: the top of low when the count is odd, or the average of both tops when it is even.

Why it is correct: by the two rules, low holds exactly the smaller half of the sorted order, so its top and the top of high are the one or two middle values.

top
low (max on top)
high (min on top)
answers
empty
  1. low, high = max_heap(), min_heap()
  2. def addNum(num):
  3. low.push(num)
  4. high.push(low.pop())
  5. if len(high) > len(low):
  6. low.push(high.pop())
  7. def findMedian():
  8. if len(low) > len(high): return low.top()
  9. return (low.top() + high.top()) / 2

low keeps the smaller half with its largest on top. high keeps the larger half with its smallest on top. The middle of the data is always on the two tops.

1 / 13
Solution.javasolution.pySolution.cssolution.gosolution.ts
// Two heaps: low holds the smaller half (largest on top), high holds the
// larger half (smallest on top). The median sits on top. log n per add.
class MedianFinder {
private final PriorityQueue<Integer> low = new PriorityQueue<>(Collections.reverseOrder());
private final PriorityQueue<Integer> high = new PriorityQueue<>();
public void addNum(int num) {
// Pass the number through low, so the largest of the small half
// moves up. This keeps every number in low <= every number in high.
low.add(num);
high.add(low.poll());
// Keep the sizes equal, or low one bigger.
if (high.size() > low.size()) low.add(high.poll());
}
public double findMedian() {
if (low.size() > high.size()) return low.peek();
return (low.peek() + high.peek()) / 2.0;
}
}
Time O(log n) per addSpace O(n)Here is how we get these from the code, in four steps.
  1. 1 Find the work that repeats

    The lit lines push into a heap or pop from it. A heap keeps its order by moving the new or removed item up or down its tree, one level per step, and the tree has about log n levels. So each push or pop costs up to log n steps. We count pushes and pops.

    1. low, high = max_heap(), min_heap()
    2. def addNum(num):
    3. low.push(num)
    4. high.push(low.pop())
    5. if len(high) > len(low):
    6. low.push(high.pop())
    7. def findMedian():
    8. if len(low) > len(high): return low.top()
    9. return (low.top() + high.top()) / 2
  2. 2 Count the runs on Example 1

    The table counts pushes and pops per add on Example 1. An add always does a push, a pop and a push. When high gets too big, it does one more pop and push.

    5 + 3 + 5 + 3 + 5 = 21 pushes and pops.

    addNum52817
    heap pushes + pops53535
  3. 3 Write the count for any n

    Each add does at most 5 pushes and pops, and each one costs at most about log n steps, where n is the count so far:

    one add: at most 5 × log n steps. findMedian: 1 step

    Check it with Example 1: no add did more than 5 pushes and pops, and the heaps never held more than 3 numbers, so each push or pop moved at most 1 level.

  4. 4 Keep only the part that grows fastest

    Drop the 5. A fixed number of heap moves does not change the growth. What is left is O(log n) per add, and O(1) for findMedian. A stream of n adds costs O(n log n), against O(n²) for the sorted list.

    So the time is O(log n) per add.

  5. S Space: count the extra memory the same way

    Every number sits in exactly one of the two heaps, so together they hold n numbers. The space is O(n).

    low (about n / 2)high (about n / 2)

2,000 random call sequences of up to 21 calls, with numbers from -3 to 3, so equal numbers and medians like 0.5 come up. The brute force and the two heaps gave the same medians every time, and they matched a simple check that sorts all the numbers.

Largest inputsResult
50,000 calls (the most allowed): 40,000 adds, many at the very ends of the range, and a median after every fourth add. Two heaps.0.03 s, and about 2 MB of extra memory: the two heaps hold every number, which is the O(n) space.
50,000 calls: 40,000 rising numbers, so every add moves a number between the heaps, then 10,000 medians. Two heaps.0.03 s.
50,000 calls, the first mix. Brute force.0.07 s, about twice as slow. Each add shifts up to 40,000 numbers, but Python does that shift as one fast memory copy, so it stays quick at this size.