Top k, medians and streaming data

A bounded heap answers questions about huge inputs in small memory.

The heap's best use is data you cannot or should not hold all at once.

Top k largest, with a min-heap

Counter-intuitively, you keep the k largest in a min-heap, so the weakest survivor is always at the top and cheapest to evict.

function topK(stream, k) {
  const heap = new MinHeap();     // holds the k largest seen so far

  for (const value of stream) {
    if (heap.size < k) heap.push(value);
    else if (value > heap.peek()) { heap.pop(); heap.push(value); }
  }

  return heap;
}

O(n log k) time and O(k) space, whatever n is. Sorting the whole input would be O(n log n) and require all of it in memory — impossible for a stream.

The rule of thumb: min-heap for the k largest, max-heap for the k smallest. Getting this backwards is the standard slip.

Running median with two heaps

Keep a max-heap of the lower half and a min-heap of the upper half, balanced to within one element. The median is then at one or both tops.

class RunningMedian {
  #low = new MaxHeap();     // smaller half
  #high = new MinHeap();    // larger half

  add(value) {
    this.#low.push(value);
    this.#high.push(this.#low.pop());              // pass the largest of low up

    if (this.#high.size > this.#low.size) {        // rebalance
      this.#low.push(this.#high.pop());
    }
  }

  get median() {
    return this.#low.size > this.#high.size
      ? this.#low.peek()
      : (this.#low.peek() + this.#high.peek()) / 2;
  }
}

O(log n) per insert, O(1) per query. The push-then-pop-then-rebalance sequence keeps the two halves correct without any special cases.

Merging k sorted lists

Hold the current head of each list in a heap of size k:

// O(N log k) for N elements across k lists — far better than
// concatenating and sorting, which is O(N log N).

This is the core of external merge sort and of log aggregation across shards.

Choosing between heap and quickselect

Heap of size k Quickselect
Time O(n log k) O(n) average
Space O(k) O(1)
Streaming Yes No
Mutates input No Yes
Worst case O(n log k) O(n²)

For an in-memory array and a one-off query, quickselect. For a stream, a bounded memory budget, or repeated queries, the heap.