Why does Timsort merge runs eagerly under stack invariants instead of merging them all at the end?
answer
- Runs are not merged in arrival order
- Something must bound how many runs are pending
- Compare run lengths near the top of the stack
- Growth at least Fibonacci-fast down the stack
- Only neighbours may ever be merged
basics
~20 sDeferring every merge would need a run stack proportional to n and would allow wildly unbalanced merges. Timsort instead keeps the top run lengths satisfying size invariants, merging as soon as they are violated, which bounds stack depth logarithmically and keeps merges between comparable-sized runs.
solid answer
~50 sDetected runs are pushed onto a stack, and after each push Timsort restores two invariants on the top entries: with lengths `Z`, `Y`, `X` from deepest to top, it requires `Z > Y + X` and `Y > X`. Violations are repaired by merging immediately, and only **adjacent** runs are ever merged — merging across an intervening run would move equal elements past each other and destroy stability. The invariants buy two things. They force run lengths to grow at least geometrically down the stack, which bounds the stack depth to O(log n) and lets implementations preallocate it. And they keep every merge between runs of comparable size, which is where merge sort's cost model is at its best; deferring all merges to the end invites merging a 64-element run into a million-element one. There is a real correctness story here too: the original collapse rule was proved insufficient by formal analysis, and implementations had to strengthen it or enlarge the stack.
code
pseudocode · 13 lines// run stack holds run lengths; X = top, Y = second, Z = third
// invariants wanted: Z > Y + X and Y > X
while stackSize > 1
if stackSize > 2 and Z <= Y + X
if Z < X
merge(Z, Y) // merge the smaller neighbour pair
else
merge(Y, X)
else if Y <= X
merge(Y, X)
else
break // invariants hold; push the next run
...go deeper
Know that detected runs wait on a stack rather than being merged the moment they appear, and that Timsort merges only neighbouring runs when their sizes get out of balance.
Explain the two length invariants and the collapse loop that restores them, and say what each buys: a stack whose depth is logarithmic in n, and merges between runs of comparable size.
Demonstrate the reasoning a reviewer needs — why adjacency is the stability requirement rather than a convenience, why the length rule bounds depth exponentially, and what the failure mode is when the invariant does not actually hold.
Own the wider lesson: a five-line invariant in a decade-old library routine held a reachable bound violation that only a proof attempt surfaced, and the choice between strengthening the check and over-allocating the stack is a real risk-versus-churn call.
## The design question Run detection produces a sequence of ordered runs. Something has to decide **when** and **in what order** to merge them. Two extremes bracket the design space. Merge every new run into the accumulated result immediately, and you get a linear chain: run 2 merged into run 1, run 3 into that, and so on. Each merge touches the whole accumulated prefix, so with `r` runs the total cost is O(r * n) — quadratic when runs are many. Defer everything and merge at the end, and you must hold all `r` runs, which is up to `n/min_run` stack entries, and you still have to decide an order. Nothing prevents a bad order. Timsort takes a middle path: push runs onto a stack and, after each push, merge just enough to restore size invariants over the top few entries. ## The invariants With the three topmost run lengths named `Z`, `Y`, `X` from deepest to shallowest, the rules are: - `Z > Y + X` - `Y > X` After pushing a run, a collapse loop checks these and merges until they hold. When `Z <= Y + X` is the violation, it merges `X` with whichever neighbour is smaller (`Z` with `Y`, or `Y` with `X`) so the result stays balanced; when only `Y <= X` is violated, it merges `Y` with `X`. ``` // stack of run lengths; X = top, Y = second, Z = third while stackSize > 1 if stackSize > 2 and Z <= Y + X if Z < X merge(Z, Y) else merge(Y, X) else if Y <= X merge(Y, X) else break ``` ## What the invariants buy **A logarithmically bounded stack.** `Z > Y + X` forces lengths going down the stack to grow at least as fast as the Fibonacci sequence, so the depth for an input of size `n` is O(log n) with a small constant — a few dozen entries even for enormous arrays. That lets an implementation preallocate a fixed-size stack instead of growing one dynamically, which is a real engineering win in a hot library routine. **Balanced merges.** Merging runs of comparable size is what keeps merge sort's total work at O(n log n). Every merge of a small run into a very large one pays a full sweep of the large run for a handful of newly placed elements. The invariants make that shape impossible: nothing tiny sits on top of something enormous for long. **Bounded memory and better locality.** Merging as you go means the temporary buffer only ever has to hold the smaller of two adjacent runs, and the data being merged was touched recently. **Stability.** Only adjacent runs are merged, ever. This is not an optimisation, it is the stability guarantee: if runs A, B, C sit in input order and you merged A with C, an element of A equal to one in B would end up positioned relative to B by accident of the merge order, and the input order among equals would be lost. Adjacency is what makes the merge's tie rule — take from the earlier run first — sufficient to preserve order globally. ## The correctness story The collapse rule above is the version that shipped for years and is the one most references describe. In 2015 a formal-methods team attempting a machine-checked proof of the algorithm found that checking only the top three entries does **not** guarantee the invariant holds all the way down the stack. Crafted inputs can leave a deeper violation unrepaired, letting run lengths grow more slowly than the analysis assumed, so the stack can exceed the preallocated bound and overflow. The consequence is worth stating precisely, because candidates guess wrong here: the output does not come back mis-sorted and equal elements do not get reordered. The failure is an out-of-bounds access on the preallocated run stack. Two fixes exist and both are in the wild — check one more entry in the collapse loop so the invariant genuinely holds, or simply enlarge the preallocated stack so overflow is unreachable. Java and Python implementations both carried the flaw and responded differently, one enlarging the stack, the other later replacing the merge-ordering policy outright with a newer near-optimal scheme. The reason to know this story is not trivia. It is a concrete demonstration that a widely deployed library routine, reviewed for a decade, carried a reachable bound violation that only a proof attempt exposed — and that the fix was a change to a five-line invariant check, not a redesign. ## What to say when asked State the two invariants, explain both payoffs (bounded stack, balanced merges), give adjacency as the stability requirement, and close with the honest note that the original rule was proved insufficient and the failure mode was stack overflow rather than incorrect output.
- Why can Timsort never merge two non-adjacent runs, even when their lengths would make it convenient?Because stability depends on it. Runs sit in input order; merging A with C skips over B, so an element of A that compares equal to one in B would be placed relative to B by accident rather than by input order. Merging only neighbours means the merge's tie rule — take from the earlier run when keys compare equal — is enough to preserve the original order among equals across the whole array.
- What exactly went wrong when the original collapse rule was proved insufficient?Checking only the top three stack entries can leave a violation deeper in the stack unrepaired. Run lengths then grow more slowly than the depth analysis assumed, so on crafted inputs the number of pending runs exceeds the preallocated stack and the write goes out of bounds. The output is never mis-sorted and stability is never lost — the failure is an overflow, fixed either by checking one more entry or by enlarging the stack.
- How does Z > Y + X bound the stack depth?It forces each run to be longer than the sum of the two above it, so lengths going down the stack grow at least as fast as the Fibonacci sequence — exponentially. Since the total of all run lengths cannot exceed n, the number of entries is O(log n) with a small base, which in practice is a few dozen even for very large arrays. That is what makes a fixed-size preallocated stack safe.
- Why not simply merge each new run into the accumulated result immediately?That degenerates into a linear chain of merges: every new run is merged against everything sorted so far, so each of the r merges sweeps most of the array and the total is O(r * n). On random input, where r is proportional to n divided by min-run, that is quadratic. Delaying merges until sizes are comparable is exactly what keeps the total at O(n log n).
saying these in an interview costs you the question
- Says runs are merged in arrival order into one accumulator
- Thinks any two runs on the stack may be merged
- Claims the invariants exist to make merges equal-sized
- Says the invariant bug produced mis-sorted output
- Cannot explain why stack depth is bounded
- Treats eager merging purely as a memory optimisation