A dataset too large to hold in memory, or a linked list with no random access, rules out the in-place array sorts that quick sort and heap sort rely on. What stays cheap is merging: combining two already-sorted sequences into one takes a single linear pass — compare the two front elements, emit the smaller, advance that side, and repeat. Merge sort turns all of sorting into that one operation. It splits the input by position until each piece holds one element — a run of length 1 is sorted by definition — then merges runs pairwise, doubling the sorted run length each pass: 1 into 2, 2 into 4, up to a single run of length n.

The merge is the only place ordering happens, and it is valid because of one fact: when both runs are sorted, the smaller of their two front elements is the smallest element neither run has placed yet. A single left-to-right pass therefore merges two runs totalling m elements in O(m) time, comparing only their fronts and never looking back. Because the split is by position rather than by value, the recursion has the same shape — ⌈log₂ n⌉ levels of n elements — for sorted, reversed, or random input, which pins the total cost at O(n log n) in every case.

Core condition: two sorted runs → one linear-time merge takes the smaller front element → ⌈log₂ n⌉ merge levels → O(n log n) time on every input, O(n) merge buffer for arrays.

Splitting, then merging

The trace sorts the eight-element array [8, 3, 5, 1, 9, 2, 7, 4].

The decisive step is the final merge. By then the left half has become [1, 3, 5, 8] and the right half [2, 4, 7, 9]; one pass compares the two fronts, emits the smaller, and advances that read head, producing [1, 2, 3, 4, 5, 7, 8, 9] after at most seven comparisons for eight elements. That same merge runs at every level below it — length-1 runs merge into length-2, then length-4 — so the number of comparisons is bounded by the number of levels, ⌈log₂ 8⌉ = 3, times the n elements each level touches. No comparison depends on how disordered the input was; it depends only on how the two current fronts relate.

Why the merge stays sorted

A merge holds one invariant: the output already contains, in sorted order, the smallest elements drawn from the two runs so far, and each run’s read head points at the smallest element that run has not yet contributed. Taking the smaller of the two heads appends the next-smallest element overall and advances one head, so both halves of the invariant survive. When one run empties, whatever remains in the other is already sorted and no smaller than anything placed, so it copies over directly.

Nothing sorts on the way down. The split only partitions indices, and the leaves are single elements that are already sorted. Every comparison happens in the merges on the way up, which is why the algorithm’s behaviour does not depend on the initial arrangement — there is no pivot to unbalance and no run to detect.

Stability rides on one comparison. The left run holds the elements that appeared earlier in the original array, so on a tie the merge must emit the left element first to keep equal keys in their original order. The implementation does this with a[i] <= a[j]: equal keys take the left branch. Switching to a[i] < a[j] pulls the right element ahead on ties and quietly makes the sort unstable — the whole distinction between a stable and an unstable merge is that one operator.

Complexity

CaseTimeAuxiliary spaceCause
BestO(n log n)O(n)Positional split gives ⌈log₂ n⌉ levels regardless of order; no early exit shortcuts a sorted input.
AverageO(n log n)O(n)Same recursion tree; element values decide which head is taken, not how many merges run.
WorstO(n log n)O(n)No partition to skew and no termination test, so the bound is a ceiling rather than an expectation.

The auxiliary figure is the merge buffer. A top-down array merge sort copies each merge’s output into an O(n) scratch array and adds O(log n) call-stack space for the recursion; a bottom-up variant merges adjacent runs of width 1, 2, 4… in loops and keeps the O(n) buffer while dropping the stack entirely. A linked-list merge sort is the outlier: it splices existing nodes by pointer instead of copying, so it needs only O(1) extra cells beyond the O(log n) stack, and it never asks for a[mid] by index.

Where the memory goes

The O(n) buffer is not incidental. A comparison-only merge of two sorted array segments cannot be done in place without extra work, because writing the next output element would overwrite an input element that has not been read yet. In-place merge variants exist — block merge and related “in-place merge sort” schemes — that keep the O(n log n) time bound while pushing auxiliary space toward O(1), but they replace the single linear copy with rotations and block swaps, run several times slower in practice, and are harder to keep stable. The standard tradeoff is therefore explicit: pay O(n) memory for a simple, fast, stable merge, or pay a constant-factor slowdown for O(1) space.

Where the input is sequential rather than indexed, the buffer stops mattering for a different reason. External merge sort streams sorted runs off disk, merges them with the same take-the-smaller-front rule, and streams the result back, so a dataset larger than RAM is sorted with predictable sequential I/O rather than random seeks. Timsort (Tim Sort) generalizes the same core: it scans the input for runs that are already ascending or descending and merges them with the identical front-comparison rule, layering galloping and a minimum run length on top. Merge sort’s merge step is the primitive both of those build on.

Reference drawer

Questions

References

  • Mergesort — Princeton Algorithms: top-down and bottom-up merge, the stability argument, and the doubling of run lengths.
  • Merge sort — stability proof, bottom-up variant, and the external / multiway merge used for out-of-core data.
  • listsort.txt — CPython’s description of Timsort: run detection and galloping merge built on the same take-the-smaller-front rule.