Find Median from Data Stream — Two-Heap Approach
Advertisement
Problem Statement
Design a data structure that supports two operations: addNum(num) adds a number from an integer data stream; findMedian() returns the median of all numbers added so far. For even count, the median is the average of the two middle elements.
Constraints:
- -10^5 <= num <= 10^5
- At most 5 * 10^4 calls to
addNumandfindMedian - At least one number is added before
findMedianis called
Input: addNum(1), addNum(2), findMedian(), addNum(3), findMedian()
Output: 1.5, 2.0Input: addNum(6), addNum(10), addNum(2), addNum(6), findMedian()
Output: 6.0Why This Problem Matters
Running medians appear in analytics pipelines, A/B test result streaming, stock price monitoring, and any system that needs real-time statistical summaries over unbounded data. Google's streaming analytics platform, Apache Flink, and Amazon Kinesis Data Analytics all implement variants of this algorithm for rolling quantile computation.
This is a canonical hard problem in FAANG interviews because it requires combining two heaps in a non-obvious way and maintaining a careful invariant across every insert. The two-heap approach achieves O(log N) per insert and O(1) median—optimal for a streaming context where you cannot store all values.
Understanding this problem also provides intuition for the general k-th order statistic in a stream, percentile tracking, and the P99 latency computation used in SLA monitoring at every major technology company.
The Core Insight
Partition numbers into two halves: lo (max-heap containing the lower half) and hi (min-heap containing the upper half). Maintain the invariant: len(lo) >= len(hi) and every element in lo is less than or equal to every element in hi.
After each addNum, push to lo then transfer lo's max to hi. If hi becomes larger than lo, transfer hi's min back to lo. This rebalancing ensures the invariant holds.
Median: if total count is odd, return lo's max. If even, return the average of lo's max and hi's min. In Python, negate values to simulate a max-heap with heapq (which is a min-heap).
Visual Dry Run
| addNum | lo (max-heap) | hi (min-heap) | Median |
|---|---|---|---|
| 1 | [1] | [] | 1.0 |
| 2 | [1] | [2] | 1.5 |
| 3 | [2, 1] | [3] | 2.0 |
| 4 | [2, 1] | [3, 4] | 2.5 |
| 5 | [3, 2, 1] | [4, 5] | 3.0 |
Solution (Optimal)
import heapq
class MedianFinder:
def __init__(self):
self.lo = [] # max-heap (negate values)
self.hi = [] # min-heap
def addNum(self, num: int) -> None:
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))
def findMedian(self) -> float:
if len(self.lo) > len(self.hi):
return float(-self.lo[0])
return (-self.lo[0] + self.hi[0]) / 2.0class MedianFinder {
constructor() {
this.lo = []; // max-heap stored as negated min-heap
this.hi = []; // min-heap
}
addNum(n) {
// Push to lo (max-heap via negation), then balance
this._pushMin(this.lo, -n);
this._pushMin(this.hi, -this._popMin(this.lo));
if (this.hi.length > this.lo.length) {
this._pushMin(this.lo, -this._popMin(this.hi));
}
}
findMedian() {
if (this.lo.length > this.hi.length) return -this.lo[0];
return (-this.lo[0] + this.hi[0]) / 2;
}
_pushMin(h, v) {
h.push(v);
let i = h.length - 1;
while (i > 0) {
const p = (i - 1) >> 1;
if (h[p] > h[i]) { [h[p], h[i]] = [h[i], h[p]]; i = p; }
else break;
}
}
_popMin(h) {
[h[0], h[h.length - 1]] = [h[h.length - 1], h[0]];
const v = h.pop();
let i = 0;
while (true) {
let m = i, l = 2*i+1, r = 2*i+2;
if (l < h.length && h[l] < h[m]) m = l;
if (r < h.length && h[r] < h[m]) m = r;
if (m === i) break;
[h[i], h[m]] = [h[m], h[i]]; i = m;
}
return v;
}
}Time: O(log N) for addNum — two heap pushes and at most two pops each
Space: O(N) — all elements stored across the two heaps
Common Mistakes
- Pushing to
hifirst instead oflo—the invariant requires all elements flow throughlofirst so that the max oflois correctly propagated tohi - Forgetting to negate values in Python:
heapqis a min-heap; push-numto simulate a max-heap forlo - Off-by-one in the size check: rebalance when
len(hi) > len(lo), not when they are equal—the invariant allowsloto have one extra element - Returning
intfromfindMedianinstead offloat—the average of two integers must use floating-point division - Not handling the single-element case: with one element in
loand nothing inhi, median islo[0]directly
Interview Tips
- Draw the two heaps explicitly and label them "lower half max-heap" and "upper half min-heap"
- State the invariant before writing any code: "lo has all smaller-half values, hi has larger-half values; lo can have one more element than hi"
- The three-step
addNumpattern is canonical: push to lo, transfer lo's max to hi, rebalance if needed - In Python, explain the negation trick for max-heap simulation upfront—interviewers who don't know Python may be confused
Follow-up Questions
- How would you find the k-th smallest element in a stream instead of the median? (Use a min-heap of size k; the top is always the k-th largest seen so far)
- How would you handle a sliding window median (last N elements only)? (Two heaps plus a lazy deletion set to remove expired elements)
- What if the stream has a skewed distribution with mostly small numbers? (The invariant self-adjusts; no additional logic needed)
- How would you compute the 99th percentile latency in a stream? (Maintain two heaps with a 99/1 size ratio instead of 50/50)
- How would you make this thread-safe for concurrent
addNumcalls? (Lock both heaps together atomically; or use a lock-free priority queue)
Key Takeaways
lois a max-heap (lower half) andhiis a min-heap (upper half); maintainlen(lo) >= len(hi)addNumalways pushes tolofirst, transferslo's max tohi, then rebalances ifhigrows larger- In Python, simulate a max-heap by negating all values pushed to
lo findMedianreturnslo's max for odd total count, or the average oflo's max andhi's min for even count- This achieves O(log N) insert and O(1) median—optimal for an unbounded data stream
- The same two-heap partition generalises to any percentile tracking: change the size ratio from 50/50 to p/(1-p)
- LeetCode 295 is the canonical version; Google and Microsoft use variants with sliding window constraints
Advertisement