Merge sort
Split until trivially sorted, then merge sorted halves. The canonical divide-and-conquer, and the sort that guarantees O(n log n). Stable, but needs O(n) space.
- Time:
- O(n log n)
- Space:
- O(n)
- Worst:
- O(n log n)
The problem it solves
Some sorting jobs cannot tolerate a bad day. A database engine sorting a join, a system sorting records by one key and then another without scrambling the first pass, a process sorting far more data than fits in memory — these need three properties quicksort can’t promise: a guaranteed O(n log n) whatever the input, stability (equal elements keep their relative order), and a shape that works on sequential access (streams, linked lists, disk runs). Merge sort delivers all three, and charges O(n) extra space for the privilege.
That bill explains the division of labour in real standard libraries: quicksort-family for primitive arrays where stability is meaningless, merge-family (TimSort) wherever stability is owed.
The intuition — and where it breaks down
Two piles of exam papers, each already sorted by grade. Combining them is mindless: compare the two top papers, take the better one, repeat. You never look deeper than the tops, because sorted piles put their best candidate on top. Merging two sorted piles of total size n costs n comparisons at most — one per paper taken.
Merge sort’s move is to conjure sorted piles out of nowhere: a single paper is trivially a sorted pile. So split the stack in half, keep splitting until every pile is one paper, then merge pairs of piles back up. Each level of merging doubles pile sizes; log₂ n levels later, one sorted stack.
Where the analogy breaks: with paper, “splitting” is free and “merging” happens into your hands. In an array, merging needs somewhere to put the output — you cannot interleave two adjacent sorted halves in place without stomping on unread elements. That’s the O(n) buffer, and it isn’t incidental; in-place merging exists in the literature but is complicated enough that nobody ships it. The buffer is the price of the guarantee.
A walkthrough you can check
Sort [8, 3, 5, 1].
- Split to
[8, 3]and[5, 1]; split again to singletons — four trivially-sorted piles. - Merge
[8]and[3]: fronts are 8 and 3, take 3, then 8 →[3, 8]. Merge[5]and[1]→[1, 5]. - Merge
[3, 8]and[1, 5]: fronts 3 vs 1 → take 1. Fronts 3 vs 5 → take 3. Fronts 8 vs 5 → take 5. Left half exhausted → drain 8. Result[1, 3, 5, 8].
Note what the final merge cost: three comparisons and four writes for four elements. Every element is written once per level, and comparisons never exceed writes. The visualization shows the two halves highlighted as regions and asks you, mid-merge, which front gets taken — the answer is always just “the smaller of two numbers”, which is the point: merge sort is a big result assembled from the smallest possible decision.
The invariant
Every range handed to a merge is already sorted, and the merge’s output is sorted because it only ever appends the smallest remaining candidate. The recursion makes the first half true (singletons are sorted; merged ranges are sorted by the second half); the second half is true because both remaining candidates’ minimums are sitting at the fronts.
The tie-break rule hides inside that invariant: when fronts are equal, take from the left half. That single convention is the entirety of merge sort’s stability — the left half’s elements came earlier in the original array, so taking left-first preserves original order among equals. Write < instead of <= in one comparison and stability silently dies; no small test catches it unless the test sorts objects by one field and checks another.
Complexity, derived
Two factors, both visible in the trace. Per level: the merges at any depth cover disjoint ranges whose sizes sum to n, and each merge writes each of its elements exactly once — n writes per level, at most n comparisons. Levels: ranges halve going down, so ⌈log₂ n⌉ levels of splitting and the same coming back up. Multiply: O(n log n), unconditionally — the prediction prompt in the visualization asks you to compute a merge’s write count in advance precisely because “size of the range, exactly” is the fact the whole bound stands on.
Compare against the reversed and random presets: the comparison counter barely moves between them. That indifference is the guarantee, and it’s also the criticism — merge sort does full work on inputs that are nearly sorted already, which is the gap TimSort exploits by detecting existing runs.
Space: the O(n) buffer, plus O(log n) recursion stack. For linked lists the buffer vanishes — merging relinks nodes in place — which is why merge sort is the list sort.
What people get wrong
Computing the midpoint as (lo + hi) / 2 in fixed-width languages — same overflow as binary search, same fix: lo + (hi − lo) / 2.
Copying at the wrong moment. The merge must read from a snapshot of the range while writing into the range; reading and writing the same live cells interleaves garbage. Copy the range into the buffer first, then merge buffer → array.
“It’s O(log n) space because recursion.” The recursion is O(log n); the buffer is O(n), and the buffer dominates. Claiming O(log n) total space in an interview undoes an otherwise good answer.
Allocating a fresh buffer per merge. Correct but slow — one shared buffer allocated once serves every merge, and the difference is measurable at scale.
Implementation notes across languages
Python’s and JavaScript’s built-in sorts are TimSort — merge sort with run detection and insertion sort below a threshold; when asked “how does your language sort?”, that is the answer, and “merge-based, therefore stable” is the follow-through. Java uses TimSort for objects (stability contract) but not primitives (no observable stability) — a distinction interviewers enjoy. For linked lists in any language, bottom-up merge sort (merge runs of 1, 2, 4…) avoids recursion entirely and needs O(1) extra space; the top of LeetCode’s “Sort List” problem is exactly this. External sorting — data on disk — is merge sort’s other kingdom: sort memory-sized chunks, then k-way merge the sorted runs with a heap.
Why this visualization
The scratch buffer is drawn: each merge lifts a copy of its range into the buffer row — the O(n) cost the header claims — and the two runs drain from their L and R fronts as the smaller element drops into the bracketed output range at OUT. The level diagram beneath shows every split range and fills in as ranges merge back up; its deepest row is the max-depth counter, drawn.
When to reach for it
When the O(n log n) guarantee or stability is non-negotiable, when sorting linked lists (no random access needed), or when data does not fit in memory — external sort is merge sort. Quicksort usually wins on in-memory arrays because of constants and cache behaviour.
The follow-up questions
What interviewers ask after "implement merge sort" — with answers.
- Why is merge sort preferred for linked lists?
- Merging needs only sequential access, so lists merge in O(1) extra space by relinking, while quicksort needs the random access lists do not have. Splitting is a slow/fast pointer walk.
- Can it be done in O(1) extra space on arrays?
- In-place merging exists but is complicated and loses either stability or the clean O(n log n) bound in practice. The honest answer is that the O(n) buffer is the price of the guarantee.
- Where does the n log n come from?
- log n levels of splitting, and each level does O(n) total merge work. The recursion tree makes both factors visible.
Where it goes wrong
- Using `<` instead of `<=` when comparing the two fronts, which destroys stability.
- Computing the midpoint as (lo + hi) / 2 in a language where that overflows.
- Forgetting to copy the range into a buffer before overwriting, corrupting the merge.
Test yourself
16 interview questions on merge sort — complexity, trade-offs, edge cases and invariants — as flip cards or a scored quiz, with the answers linking back to the exact step of the trace above.
Problems built on this pattern
- Sort an Array
- Merge Two Sorted Lists
- Count of Smaller Numbers After Self
- Sort List
Related algorithms
- QuicksortPartition around a pivot that lands exactly where it belongs, then recurse on each side.
- Heap sortBuild a max-heap in place, then repeatedly swap the maximum to the end.
- Insertion sortGrows a sorted prefix by inserting each value into it.
- Bubble sortRepeatedly walks the array swapping out-of-order neighbours.