One stream of values arrives, one per tick, and two panes compete to keep the k largest. The left pane sorts everything — binary insertion into a growing sorted array, the first k bracketed as the answer it will read off at the end. The right pane holds a min-heap that never grows past k, drawn as a small lattice whose root is the smallest value it kept: the door. Every arrival visibly does one of three things there — bounces off the root and dies after a single comparison, displaces the root and sifts down into place, or (while the heap is still filling) walks in and sifts up.
A comparison counter ticks under each pane. That is the whole point: the complexity table says O(n log n) against O(n log k), but the picture shows why — the right pane’s bounce rate climbs toward “almost everything dies at the door” as the stream runs, because once the heap holds k good values a random newcomer rarely beats the worst of them. The bounded structure wins by refusing work, not by doing it faster.
Tune k (1–15) and the stream length, drive it by hand with step, or let it run. The final readout gives the two totals and their ratio.
Reuse
src/sift.js is a framework-free ES module:
makeStream(n, max = 99)— n random integers in [1, max]; stand-ins for frequency counts.sortedInsert(sorted, value)— binary insertion into a descending array, returning{ index, comparisons }.BoundedMinHeap(k)—offer(value)returns{ kind: 'fill' | 'bounce' | 'displace', path, comparisons, evicted };pathis the array indices the value touched, which is what makes the sift animatable.simulate(values, k)— runs both strategies and returns one frame per arrival with snapshots of both panes, so a renderer can scrub in any order. Same trace-per-tick shape asrhoandastar-grid.
No rendering or timers in the module; the canvas demo is reference code.
Gotchas
- Comparison counts are honest but not canonical: the sort pane counts binary-search probes (not the element shifts), and the heap’s sift-down counts both the which-child-is-smaller check and the sink check. Change the accounting and the ratio moves; the shape of the divergence doesn’t.
- k > n is legal — the heap just never fills, every arrival is a
fill, and nothing bounces. That is correct, not a bug, and a useful thing to see once. - The demo bundles its own copy of
sift.js(self-contained by contract); re-copy after editingsrc/.