A feed produces millions of latency samples and a dashboard needs the ten slowest. Sorting the whole feed answers the question in O(n log n) and forces every sample into memory at once, yet nine hundred and ninety-nine of every thousand comparisons order values that never appear in the result. The task only needs a fixed k, not a full ranking.

Keeping a size-k heap while scanning removes that waste. To surface the k largest values the heap is a min-heap: its root is the smallest of the k best elements seen so far — the weakest survivor. A new element only matters if it beats that root, and when it does the root is evicted and the newcomer inserted, so the heap size never leaves k. After one pass the heap holds exactly the k largest, and it held no more than k elements at any moment, so the input can arrive as a stream. The symmetric problem — the k smallest — uses a max-heap the same way.

Core shape: k ≪ n, possibly streaming → a size-k min-heap whose root is the weakest survivor → O(n log k) time, O(k) space.

Visualization pending

Planned StepTrace: a size-k min-heap card showing each input element compared against the heap’s smallest and replacing it when larger, so the heap always holds the k largest seen so far. No matching renderer exists in engine.js yet.

Why a min-heap holds the largest

The invariant carried across the scan is that the heap contains the k largest of every element seen so far, with the k-th largest at the root. The first k elements fill the heap outright. Each later element x faces one comparison against the root:

  • x > root proves x belongs to the top k — it beats the current weakest survivor. The root leaves, x enters, and the heap again holds the k largest seen so far.
  • x ≤ root proves x cannot belong to the top k — it is no larger than an element already outside it. Discarding x preserves the invariant untouched.

The polarity is the load-bearing choice. Exposing the minimum of the retained set at the root is what makes the “does this element deserve to be kept” test a single O(1) peek, and what makes eviction remove the right element. A max-heap of size k would expose the largest retained element, which is never the one to drop, so it cannot support this scan.

Because the heap never exceeds k entries, each insert and evict is O(log k) rather than O(log n). Over n elements that is O(n log k) time, and the resident set is O(k) regardless of how large n grows — the property that lets the input be a stream rather than a materialized array.

For a static, in-memory array where the order among the top k is irrelevant, Quickselect partitions around a pivot and recurses into only the side holding the k-th position, discarding the other half. The problem size falls geometrically, giving O(n) average time, at the cost of mutating the array and holding all of it in memory.

Complexity

Finding the k largest of n elements:

ApproachTimeAuxiliary spaceCause
Size-k min-heapO(n log k)O(k)n elements each cost one O(1) peek and, at most, one O(log k) replace on a heap capped at k.
QuickselectO(n) average, O(n²) worstO(1)Geometric shrink from partitioning gives linear average; adversarial pivots leave the size nearly unchanged each step. A median-of-medians pivot forces O(n) worst case at a larger constant.
Full sortO(n log n)depends on sortRanks all n elements when only k are needed.

Quickselect’s space is O(1) extra beyond the input it mutates in place; its average bound assumes a randomized or otherwise non-adversarial pivot. The heap’s O(k) is the only column that stays bounded when n is unbounded, which is why it is the streaming choice.

When the assumptions stop holding

Using a max-heap for the k largest inverts the mechanism. A max-heap of all n elements — pop k times — does return the right answer, but it holds every element, so its O(n) resident set defeats the whole point of the pattern: no streaming, and no memory saving over materializing the input. Its time is not the problem — heapify builds in O(n) and k extractions cost O(k log n), so the total is O(n + k log n), which for k ≪ n is roughly O(n) and actually beats a full sort. The space is what disqualifies it against the size-k heap’s O(k). A size-k max-heap is worse still: its root is the strongest retained element, so the comparison keeps the wrong side and the scan collects the k smallest. The result passes symmetric test data and fails everything else.

k ≥ n erases the advantage. Once the heap must hold every element, O(n log k) is O(n log n) and the extra bookkeeping only adds constant-factor overhead over sorting outright.

Quickselect cannot run on a stream. It needs the whole array addressable to partition it, and it reorders that array as a side effect, so an input that arrives incrementally or must stay immutable rules it out. Its O(n²) worst case is a second constraint: a naive pivot on already-sorted or adversarial input barely shrinks the problem each step, the same degradation as Quick Sort. A randomized pivot makes that improbable; median-of-medians makes it impossible at a higher constant.

Reference drawer

Comparison

Selecting the k largest of n elements:

ApproachTimeSpaceInput assumptionStronger caseWeaker case
Full sort (Heap Sort / Quick Sort)O(n log n)O(n) residentWhole input in memoryThe full ordering is also neededk ≪ n with no need for the rest
Size-k min-heapO(n log k)O(k)Elements arrive one at a time; nothing elseStreaming input or k ≪ nk near n, where it degenerates to a sort
QuickselectO(n) average, O(n²) worstO(1) extra, mutates inputWhole array addressable and mutableStatic array, fastest single top-k, order within top k irrelevantStreams, immutable input, or adversarial data without a good pivot

A size-k heap is the fit for streaming input or k ≪ n: it pays a log k factor per element to keep the resident set at O(k), the only cost model that survives an unbounded feed. Quickselect is faster for a static in-memory array when the order among the top k does not matter, trading stream support and a worst-case guarantee for a linear average. A full sort wins only when the complete ordering is part of the result, since it does strictly more work than either selection method to produce it.

Questions

References