PrepStack · Markdown HTML

Two heaps for streaming median

Heap, selection, and stream · HTML

Formal shape

lower is a max-heap and upper a min-heap. Require max(lower) <= min(upper), with equal sizes or exactly one extra item in lower.

Recognition signals

False friends

SignalWhy it is different
One query on a static arrayQuickselect is expected linear; two heaps pay log n to preserve an online answer.
Sliding-window medianThe skeleton still works, but it needs delayed deletion, logical heap sizes, and root pruning.
A tiny bounded value domainFrequency buckets and rank scans may be simpler and support deletion naturally.

Core invariant

Every lower-half item is no greater than every upper-half item; lower has the same size as upper or exactly one extra item.

State, transition, and processing order

ItemDefinition
StateA negated-value heap for lower and a min-heap for upper; deletion variants add delayed counts and logical sizes.
TransitionPlace the new value according to the lower root, then move the root of an oversized side across the boundary.
Frontier / orderThe lower root is the greatest lower-half value and the upper root is the least upper-half value; together they are the median boundary.

Python skeleton

from heapq import heappop, heappush

class MedianStream:
    def __init__(self):
        self.lower, self.upper = [], []

    def add(self, x):
        if not self.lower or x <= -self.lower[0]:
            heappush(self.lower, -x)
        else:
            heappush(self.upper, x)
        if len(self.lower) > len(self.upper) + 1:
            heappush(self.upper, -heappop(self.lower))
        elif len(self.upper) > len(self.lower):
            heappush(self.lower, -heappop(self.upper))

    def median(self):
        if not self.lower:
            raise ValueError('empty stream')
        if len(self.lower) > len(self.upper):
            return -self.lower[0]
        return (-self.lower[0] + self.upper[0]) / 2

Correctness sketch

  1. Boundary-based insertion preserves cross-heap ordering.
  2. If one side grows too large, its root is the closest item to the split and the only item that should cross; moving it restores size without breaking order.
  3. The size convention places the middle one or two ranks at the roots, so the query formula is correct.

Complexity

TimeSpaceParameters
O(log n) per add and O(1) per medianO(n)n is the number of currently valid values.

Edge cases

Error patterns

PatternWhy it fails / fix
Balancing sizes onlyTwo arbitrary equal halves do not expose a median; cross-heap order is a separate invariant.
Losing a lower-heap signComparisons, transfers, and returns must distinguish stored negatives from logical values.
Removing a window value with list.removeThat destroys heap bounds. Use delayed counts and logical sizes.
No convention for the extra itemInsertion, balancing, and odd-length queries become inconsistent unless one side is designated.

Selection and exclusion rules

RelationTemplateBoundary

Original micro-example

PartDetail
ResultThe middle boundaries are 4 and 6, so the median is 5.
SetupTemperatures arrive as 4,10,6,2.
Trace4 enters lower, 10 upper; 6 enters upper then moves to lower; 2 enters lower and pushes 6 back to upper.

Recall prompts

My notes