Merge Sort
Split the array in half, sort each half recursively, then merge the two sorted halves in linear time.
Overview
Merge sort is the canonical Divide and Conquer sort: split into halves, sort each recursively, and merge the two sorted halves with a linear two-pointer sweep. It runs in O(n log n) in every case, is stable, and its access pattern is sequential, which makes it the basis for external sorting on disk and for sorting linked lists.
Its cost is O(n) auxiliary space for the merge buffer (in-place variants exist but are slow or complex). It is not adaptive in the textbook version, though checking a[mid] <= a[mid+1] before merging skips already-ordered halves. TimSort — Python's sorted, Java's Arrays.sort for objects — is an adaptive, run-detecting merge sort.
Intuition
A mental model before the formal terms.
Two sorted stacks of exam papers can be combined into one sorted stack by repeatedly taking whichever top paper has the smaller number — one glance per paper. To sort an unsorted stack, split it into halves until each stack has one paper (trivially sorted), then combine stacks upward. Every paper participates in log₂ n merges.
How it works
- If the range has fewer than two elements, return (base case).
- Compute
midand recursively sort[lo, mid]and[mid + 1, hi]. - Merge: with pointers
i,jat the start of each half, copy the smaller element (taking from the left on ties for stability) into a buffer; append whatever remains. - Copy the buffer back into
[lo, hi]. - Bottom-up variant: merge runs of size 1, 2, 4, … iteratively, avoiding recursion.
Why it works
Merging two sorted sequences is correct by induction: the smaller of the two heads is the smallest remaining element overall, since each sequence is sorted.
Recurrence T(n) = 2T(n/2) + O(n) solves to O(n log n) by the Master theorem: log₂ n levels, each doing O(n) merge work.
Stability: when a[i] == b[j], the left element is copied first, preserving original relative order across all merge levels.
Recognition
How to tell a problem wants this.
- A stable
O(n log n)sort is required (sorting records by one field while preserving another order). - Sorting a linked list — merge sort needs no random access and runs in
O(1)extra space on lists. - Counting inversions, "count of smaller numbers after self", reverse pairs — problems solved by instrumenting the merge step.
- Data does not fit in memory (external sort) or guaranteed worst-case time is needed.
- Merging
ksorted lists.
Interactive visualization
Play, step, change the input. ← → and space work too.
1mergeSort(a, lo, hi):2 if lo >= hi: return3 mid = (lo + hi) // 24 mergeSort(a, lo, mid)5 mergeSort(a, mid+1, hi)6 merge(a, lo, mid, hi):7 i = lo, j = mid+1, buf = []8 while i <= mid and j <= hi:9 if a[i] <= a[j]: buf.push(a[i++]) else buf.push(a[j++])10 append leftovers of both halves to buf11 copy buf back into a[lo..hi]Pseudocode
1mergeSort(a, lo, hi):2 if lo >= hi: return3 mid = lo + (hi - lo) // 24 mergeSort(a, lo, mid); mergeSort(a, mid + 1, hi)5 i = lo, j = mid + 1, buf = []6 while i <= mid and j <= hi:7 if a[i] <= a[j]: buf.push(a[i++]) else: buf.push(a[j++])8 append remaining a[i..mid] and a[j..hi] to buf9 copy buf into a[lo..hi]Implementations
1def merge_sort(a: list[int]) -> None:25 · Entry point allocates the buffer once3 buf = [0] * len(a)4 51 · Recursive sort over [lo, hi] with a shared buffer6 def sort(lo: int, hi: int) -> None:7 if lo >= hi:8 return92 · Split and recurse on both halves10 mid = lo + (hi - lo) // 211 sort(lo, mid)12 sort(mid + 1, hi)13 if a[mid] <= a[mid + 1]: # halves already ordered: skip merge14 return153 · Merge the two sorted halves into buf16 i, j, k = lo, mid + 1, lo17 while i <= mid and j <= hi:18 if a[i] <= a[j]: # ties -> left: stable19 buf[k] = a[i]20 i += 121 else:22 buf[k] = a[j]23 j += 124 k += 125 while i <= mid:26 buf[k] = a[i]27 i += 128 k += 129 while j <= hi:30 buf[k] = a[j]31 j += 132 k += 1334 · Copy the merged range back34 a[lo:hi + 1] = buf[lo:hi + 1]35 36 sort(0, len(a) - 1)- The nested
sortclosure capturesaandbuf; Python closures can read outer lists and mutate them in place withoutnonlocal. (hi - lo) // 2is floor division; Python ints never overflow so(lo + hi) // 2would also be safe.- The skip-merge check makes sorted input cheap.
- The merge writes into
bufby index;<=keeps ties on the left (stable). - Copy-back uses slice assignment
a[lo:hi + 1] = buf[lo:hi + 1], which creates a temporary O(hi - lo) list — an extra copy per merge that C++ avoids.
Slice assignment allocates a temporary per merge (O(n) per level, O(n log n) total copies, still O(n) live memory). The common merge_sort(a[:mid]) style allocates fresh lists per level too.
sorted()/list.sort()are TimSort — a stable, adaptive merge sort in C — so hand-written merge sort is only for learning or for instrumented merges (inversion counting).heapq.merge(*iterables)merges already-sorted iterables lazily, useful for external sorts.- Recursion depth is log2(n), far below the default limit of 1000.
- Using
left.pop(0)in the merge — O(n) per pop. - Using
<instead of<=, which breaks stability. - Mixing the return-a-new-list style with in-place mutation.
- Rebinding
buf = ...inside the closure withoutnonlocal(creates a local, breaks the algorithm).
- Buffer cost: the C++ version allocates one
std::vectorand merges in contiguous memory; Python slice assignment copies the merged range again per merge, and the populara[:mid]slicing style allocates O(n) per recursion level in Python and JS (slice). - Stability: the hand-written merge is stable in all four languages thanks to
<=. Library equivalents:std::stable_sort(C++),Array.prototype.sort(stable since ES2019), Pythonsorted(TimSort, always stable);std::sortis NOT stable. - Midpoint overflow:
(lo + hi) / 2can overflowintin C++; JS numbers are doubles (safe to 2^53); Python ints are unbounded. - JS
[10, 2, 5].sort()compares as strings; this merge sort compares numerically like C++ and Python. - Linked lists: C++
std::list::sortand a hand-written Python/JS list merge use O(1) extra space; the array version needs O(n).
Complexity
O(1) extra space on linked lists. Stable, not in-place, not adaptive (unless run detection is added). O(log n) recursion depth.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Guaranteed
O(n log n)with stability — sorting objects by key, multi-key sorts done in passes. - Linked lists (no random access;
O(1)extra space). - External sorting of data larger than memory; the merge step streams sequentially.
- Divide-and-conquer counting problems: inversions, count-smaller-after-self, reverse pairs.
- Parallel sorting — halves are independent.
- Memory is tight and stability is not needed — Heap Sort is
O(1)space, Quick Sort is faster on average. - Small arrays — use Insertion Sort.
- Integer keys in a small range — Counting Sort or Radix Sort run in linear time.
Alternatives
Common mistakes
- Using
<instead of<=when comparing heads — takes from the right on ties and destroys stability. - Allocating a new buffer at every recursion level (
O(n log n)allocations); allocate once and reuse. - Off-by-one on
mid: the halves must be[lo, mid]and[mid + 1, hi], andmidmust belo + (hi - lo) / 2so both halves shrink. - Forgetting to copy the leftover tail of one half after the other is exhausted.
Interview patterns
- Count inversions: during merge, when taking
a[j]from the right, addmid - i + 1to the count. - Sort a linked list in
O(n log n): split with fast/slow pointers, merge by relinking. - Merge
ksorted lists by pairwise merging in a tournament (or a heap). - Count of smaller numbers after self / reverse pairs (LeetCode 315, 493).
- Recognizing the approach from an array and a targetIntermediate
- Where does O(n log n) come from?Beginner
- Array versus linked listBeginner
- Average case versus worst caseIntermediate
- Two SumBeginner
- Kth Largest Element in an ArrayIntermediate