Two-Heap Pattern
The median of a sorted list of numbers is the middle value. Finding it naively requires sorting — \(O(n \log n)\). But what if numbers arrive one at a time in a stream and you need the median after every insertion?
The two-heap pattern solves this beautifully. Picture a sorted line of people split in the middle: the left half are all shorter than everyone in the right half. You keep two heaps:
- Max-heap (lower half): the largest element is instantly accessible at the root — that's the middle-left value
- Min-heap (upper half): the smallest element is instantly accessible at the root — that's the middle-right value
The median is either the root of the larger heap (odd total count) or the average of both roots (even total count).
Invariant to maintain
After every insertion you must restore two invariants:
- Size balance: |max_heap.size - min_heap.size| ≤ 1 — the heaps are at most 1 apart in size
- Value ordering: max_heap.top() ≤ min_heap.top() — every element in the lower half is ≤ every element in the upper half
← → arrow keys to step · click elements to interact
Median Finder (Two Heaps)
Algorithm
Why two heaps and not one?
A single heap gives \(O(1)\) access to only one extreme — the min or the max. The median is in the middle, which a single heap can't expose efficiently. Two heaps partition the data so both "halves" of the middle are at heap roots — \(O(1)\) median always.
Variant: Sliding window median
LeetCode 480 extends this to a sliding window: maintain the two-heap invariant while also removing elements that leave the window. Lazy deletion (mark elements as deleted, skip them when they reach the root) keeps the amortized cost at \(O(\log n)\) per step.