Topics › Heaps › Hard Problems
Find Median in a Stream
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
Integers arrive one at a time. Design a structure that supports two operations:
addNum(num)stores the next integer of the stream.findMedian()returns the median of all integers stored so far, as adouble.
The median is the middle value of the sorted data. When the count is even there is no single middle value, so the median is the average of the two middle values.
Constraints (LeetCode): -10^5 ≤ num ≤ 10^5, at most 5 · 10^4 calls in total, and findMedian is only called after at least one addNum. The code here accepts the full int range and throws IllegalStateException if asked for the median of an empty stream.
Example 1
addNum(1); addNum(2)
findMedian() -> 1.5 // sorted [1, 2]: average of 1 and 2
addNum(3)
findMedian() -> 2.0 // sorted [1, 2, 3]: the middle value
Example 2 (median after every add)
add 5 -> 5.0 // [5]
add 15 -> 10.0 // [5, 15]
add 1 -> 5.0 // [1, 5, 15]
add 3 -> 4.0 // [1, 3, 5, 15]: (3 + 5) / 2
All three designs below implement the same small interface, so one test script drives them all:
interface MedianFinder {
void addNum(int num);
double findMedian();
}
Approach 1: Brute force, sort on every query
Intuition. The median is defined on sorted data, so the most direct design stores numbers as they come and sorts them whenever the median is requested. Adding is trivial. The weakness is obvious: if one number arrives between two queries, the second query re-sorts everything even though the data barely changed.
Algorithm.
addNum: appendnumto a list.findMedian: copy the list into an array and sort it. Withnelements, returna[n/2]whennis odd, otherwise the average ofa[n/2 - 1]anda[n/2].
static class SortOnQuery implements MedianFinder {
private final List<Integer> nums = new ArrayList<>();
public void addNum(int num) {
nums.add(num);
}
public double findMedian() {
int n = nums.size();
if (n == 0) throw new IllegalStateException("stream is empty");
int[] a = new int[n];
for (int i = 0; i < n; i++) a[i] = nums.get(i);
Arrays.sort(a);
if (n % 2 == 1) return a[n / 2];
return ((long) a[n / 2 - 1] + a[n / 2]) / 2.0; // long: the sum can overflow int
}
}
Complexity. addNum is O(1) amortised. findMedian is O(n log n) for the sort. Space O(n). If every add is followed by a query, n operations cost O(n^2 log n) in total.
Approach 2: Better, keep the list sorted at all times
Intuition. Instead of sorting from scratch at every query, maintain the invariant “the list is always sorted”. A new number then only has to be placed in the right position, which binary search finds in O(log n). The median is read directly from the middle indices.
The catch is the insertion itself: ArrayList.add(index, value) has to shift every element after index one step to the right. So binary search reduces the comparisons, but the move cost is still linear. In practice the shift is a single fast memory copy, so this design is far quicker than approach 1, but its worst case is still O(n) per add.
Algorithm.
addNum: binary search for the first index whose value is greater thannum(inserting after equal values keeps the search simple), then insert there.findMedian: read the middle element, or average the two middle elements.
static class SortedInsert implements MedianFinder {
private final List<Integer> sorted = new ArrayList<>();
public void addNum(int num) {
int lo = 0, hi = sorted.size(); // find the first index holding a value > num
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (sorted.get(mid) <= num) lo = mid + 1; else hi = mid;
}
sorted.add(lo, num); // shifts the tail one step right: O(n)
}
public double findMedian() {
int n = sorted.size();
if (n == 0) throw new IllegalStateException("stream is empty");
if (n % 2 == 1) return sorted.get(n / 2);
return ((long) sorted.get(n / 2 - 1) + sorted.get(n / 2)) / 2.0;
}
}
Complexity. addNum is O(log n) comparisons plus O(n) shifting, so O(n). findMedian is O(1). Space O(n).
Approach 3: Optimal, two heaps
Intuition. Look at what the median actually depends on. Split the sorted data into a smaller half and a larger half. The median is determined entirely by the largest value of the smaller half and the smallest value of the larger half. The order inside each half is irrelevant. We are paying for a fully sorted list when all we need are the two values next to the boundary.
A heap is the structure that gives cheap access to one extreme value and nothing more:
lowis a max-heap holding the smaller half, solow.peek()is its largest value.highis a min-heap holding the larger half, sohigh.peek()is its smallest value.
We maintain two invariants:
- Order: every value in
lowis≤every value inhigh. - Size:
low.size() == high.size()orlow.size() == high.size() + 1.
With both in place, the median is low.peek() when the total count is odd (the extra element sits in low) and the average of the two peeks when it is even.
Why insertion keeps the order invariant. If num ≤ low.peek(), then num is no larger than the biggest element of the small half, which is no larger than anything in high, so num may go into low. Otherwise num is larger than everything in low, so it may go into high. Rebalancing moves low’s maximum to high, or high’s minimum to low. Each moved value is exactly the one at the boundary, so the order invariant survives the move, and the size invariant is restored.
Algorithm.
addNum: pushnumintolowiflowis empty ornum ≤ low.peek(), otherwise intohigh.- If
lownow has two more elements thanhigh, movelow.poll()intohigh. Ifhighis now larger thanlow, movehigh.poll()intolow. findMedian: iflowis larger, returnlow.peek(); otherwise return the average oflow.peek()andhigh.peek().
static class TwoHeaps implements MedianFinder {
private final PriorityQueue<Integer> low = new PriorityQueue<>(Collections.reverseOrder()); // smaller half, max on top
private final PriorityQueue<Integer> high = new PriorityQueue<>(); // larger half, min on top
public void addNum(int num) {
if (low.isEmpty() || num <= low.peek()) low.offer(num); else high.offer(num);
if (low.size() > high.size() + 1) high.offer(low.poll()); // rebalance: low may hold one extra
else if (high.size() > low.size()) low.offer(high.poll());
}
public double findMedian() {
if (low.isEmpty()) throw new IllegalStateException("stream is empty");
if (low.size() > high.size()) return low.peek();
return ((long) low.peek() + high.peek()) / 2.0;
}
}
Dry run on Example 2:
| add | goes into | rebalance | low (max-heap) | high (min-heap) | median |
|---|---|---|---|---|---|
| 5 | low (empty) | none | {5} | {} | 5.0 |
| 15 | high (15 > 5) | none | {5} | {15} | 10.0 |
| 1 | low (1 ≤ 5) | none | {5, 1} | {15} | 5.0 |
| 3 | low (3 ≤ 5) | low is 2 bigger: move 5 up | {3, 1} | {5, 15} | (3 + 5) / 2 = 4.0 |
Complexity. addNum is O(log n): at most three heap operations. findMedian is O(1): two peeks. Space O(n) across both heaps.
Edge cases and pitfalls
- Integer division.
(a + b) / 2withintoperands truncates3and4to3. Divide by2.0to get3.5. - Overflow in the average. Two values near
Integer.MAX_VALUEoverflowintwhen added. All three designs add inlong; the tests include[MAX, MAX]and[MIN, MAX], whose median is-0.5. - Empty stream. There is no median. The code throws
IllegalStateExceptioninstead of returning a made-up value such as0. - Duplicates. Equal values may end up on either side of the boundary, which is harmless because the median only reads the boundary values. Using
<=when choosing the heap keeps ties inlow. - Unbalanced heaps. Forgetting the rebalancing step leaves the correct values in the wrong heap; the peeks then stop being the middle elements. Checking sizes after every insertion is mandatory.
- Follow-ups. If every number is in
[0, 100], keep 101 counters and walk them to find the middle in constant time. If 99% of the numbers are in that range, use the counters for the range plus separate counts (or small heaps) for outliers below and above it.
Related problems
- Find Median from Data Stream is the same design problem as it appears in the heaps section.
- Kth largest element in a stream of running integers keeps a single heap that tracks one boundary instead of two.
- K-th largest element in an unsorted array. answers an order-statistic query on a static array.
- Median of 2 sorted arrays finds a median by partitioning two sorted halves, the same “split into small and large halves” idea.
- Matrix Median finds a median without sorting by counting elements below a guess.