skip to content

Why does a running median over a live stream use two heaps rather than one sorted buffer?

level: juniorimportance: must knowfreq 78%

answer

  1. Where does the middle value live?
  2. Split the samples into two halves
  3. Each half only needs its extreme
  4. One half wants its largest on top
  5. Max-heap below, min-heap above

basics

~20 s

A max-heap holds the lower half of the samples and a min-heap the upper half, so the median sits at the heap tops: O(1) to read, O(log n) to absorb a new sample. A sorted buffer costs O(n) per insert to shift elements.

solid answer

~50 s

The median is the boundary between the smaller half of the data and the larger half, so keep exactly that boundary cheap. A max-heap stores the lower half with its **largest** value on top; a min-heap stores the upper half with its **smallest** value on top. With an odd count the larger heap's top *is* the median; with an even count it is the mean of the two tops — an O(1) read either way, and every insert is one heap push plus at most one transfer, so O(log n). A sorted buffer also reads the middle in O(1), but each insert shifts on average half the elements: the binary search for the position is O(log n), the insertion itself is O(n). A single heap of all samples is worse still — a heap surfaces only an extreme, so reading the middle means draining half of it.

go deeper

for a junior

Be ready to say which heap is which and where the median is read from: max-heap for the lower half, min-heap for the upper half, median at one or both tops. Know that the insert is O(log n) and the read O(1).

for a middle

Explain why a sorted buffer's O(n) insert is the real cost even though the position is found in O(log n), and why a single heap cannot expose a middle element at all.

for a senior

Show you would pick this only when reads are frequent relative to inserts and the value must be exact; otherwise say what you would use instead and why the extra bookkeeping is not worth it.

for a principal

Own the framing that this structure buys an O(1) read at the price of unbounded memory and no mergeability across instances, and be able to say when that price is wrong for the system.

## The problem being solved A monitoring agent receives API request durations in milliseconds, one sample at a time, and the dashboard must show the p50 — the median of everything seen so far — refreshed after every sample. Two costs matter and they pull in opposite directions: the cost of **absorbing** a new sample, and the cost of **answering** "what is the median right now". ## Why the obvious structures lose **Unsorted buffer.** Appending is O(1), but the median is not sitting anywhere in particular, so every read is a selection pass over all n samples — O(n) at best, O(n log n) if you just sort each time. On a dashboard that reads after every sample, that is quadratic work over the stream. **Sorted buffer.** Now the read is O(1): index the middle. But insertion is O(n). This is the misconception worth killing outright: locating the insertion point by binary search is O(log n), yet making room for the new value shifts on average half the elements, and shifting dominates. "The search is logarithmic" says nothing about the insert. **One heap holding everything.** A heap is a partial order, not a total one; it promises the extreme on top and nothing about the middle. Reading the median from a single heap means popping roughly n/2 times and pushing everything back — O(n log n) per read, the worst of the three. ## The two-heap split Split the samples seen so far into two halves at the median boundary: - `lower` — a **max-heap** holding the smaller half. Its top is the **largest** of the small values. - `upper` — a **min-heap** holding the larger half. Its top is the **smallest** of the large values. Two properties are maintained: every value in `lower` is at most every value in `upper`, and the sizes differ by at most one (by convention `lower` carries the extra when the count is odd). Given those, the median is immediate. Odd count: `lower` has one more element, and its top is exactly the middle value. Even count: the two middle values are the two tops, so the median is their mean. No traversal, no popping — an O(1) read that does not mutate anything. Absorbing a sample is a push into whichever half it belongs to, followed by at most one transfer of a top from the heavier heap to the lighter one. Both are O(log n), so the per-sample cost is O(log n). ## The cost picture | approach | insert | median read | memory | |---|---|---|---| | unsorted buffer | O(1) | O(n) | O(n) | | sorted buffer | O(n) | O(1) | O(n) | | single heap of all samples | O(log n) | O(n log n) | O(n) | | two heaps | O(log n) | O(1) | O(n) | All four hold every sample, so none of them wins on memory; the two-heap version wins because it is the only one cheap on **both** axes at once. ## A detail the plumbing forces on you Mainstream runtimes disagree on whether a max-heap exists at all: several (Python's and Go's standard heaps, for instance) expose only a min-oriented heap, so the lower half is kept either by negating the stored keys or by supplying a reversed ordering, while others (Java, C++) hand you an explicit comparator or a max-oriented container directly. The algorithm is identical in every case — only the way you spell "largest on top" changes. ## What the structure does not give you It gives you *one* boundary, the one you chose to maintain. It does not give arbitrary removal (heaps expose only their top), it does not give the k-th smallest for arbitrary k, and it does not merge with another agent's copy — you cannot combine two medians into a global median. Memory also grows with the stream, one slot per sample forever, which is what pushes long-running collectors toward a bounded window or an approximate estimator. The same split generalises: to track the p90 instead of the p50, keep the size ratio at 9:1 rather than 1:1 and read the top of the lower heap. The structure is unchanged; only the target sizes move.

  • Why can't a single min-heap holding every sample give you the median cheaply?
    A heap only orders each parent against its children, so the only element whose rank you know is the top. The middle element can sit anywhere in the array, at any depth, so finding it means popping about n/2 times and restoring them afterwards — O(n log n) for one read. The two-heap split pays a little on every insert so the read is free.
  • How would you serve the p90 of the latency stream with the same idea?
    Keep the same two heaps but change the target ratio: the max-heap holds the smallest 90% and the min-heap the largest 10%, and you read the top of the max-heap. Each insert changes the target sizes by at most one element, so one transfer still restores the ratio and the cost stays O(log n) insert, O(1) read.
  • Where else does this two-heap split show up besides percentiles?
    Any time you must maintain a partition around a moving boundary. Splitting a workload around a pivot capacity is the same shape: one heap holds the light tasks with the heaviest on top, the other holds the heavy tasks with the lightest on top, so you can see both sides of the boundary in O(1) and move a task across it in O(log n) as the pivot shifts.

Two stacks of index cards sorted into 'small' and 'large' piles, each pile turned so the card nearest the boundary faces up. You never re-sort a pile — you only ever look at, or move, the two facing cards.

saying these in an interview costs you the question

  • Claims a single heap's top is the median
  • Says sorted-buffer insert is O(log n) because the search is
  • Uses two min-heaps and cannot read the lower half
  • Says the median needs a full sort on every read
  • Thinks reading the median has to pop the heaps

context