Mergesort
Mergesort splits the array in half, sorts each half recursively, and merges the two sorted halves back together. The insight is that merging two already-sorted sequences is linear — you compare their two front elements and take the smaller, repeatedly.
Splitting costs nothing and produces log n levels; merging costs per level. Hence , on every input, with no worst case to worry about.

Core Concepts
| Property | Value |
|---|---|
| Best case | |
| Average | |
| Worst case | — guaranteed |
| Space | — the merge buffer |
| Stable | Yes |
| Adaptive | No, in the classic form (but see Timsort) |
| Parallelises | Well — the two halves are independent |
Mechanism
- Python
- C++
def merge_sort(a):
if len(a) <= 1:
return a
mid = len(a) // 2
left = merge_sort(a[:mid]) # sort each half
right = merge_sort(a[mid:])
return merge(left, right)
def merge(left, right):
out, i, j = [], 0, 0
while i < len(left) and j < len(right):
if left[i] <= right[j]: # <= keeps the sort stable
out.append(left[i]); i += 1
else:
out.append(right[j]); j += 1
out.extend(left[i:]) # one side is exhausted; append the rest
out.extend(right[j:])
return out
#include <algorithm>
#include <cassert>
#include <vector>
std::vector<int> merge(const std::vector<int>& left, const std::vector<int>& right) {
std::vector<int> out;
out.reserve(left.size() + right.size());
auto i = left.begin(), j = right.begin();
while (i != left.end() && j != right.end())
out.push_back(*i <= *j ? *i++ : *j++); // <= keeps the sort stable
out.insert(out.end(), i, left.end()); // one side is exhausted; append the rest
out.insert(out.end(), j, right.end());
return out;
}
std::vector<int> merge_sort(const std::vector<int>& a) {
if (a.size() <= 1) return a;
auto mid = a.begin() + static_cast<std::ptrdiff_t>(a.size() / 2);
auto left = merge_sort(std::vector<int>(a.begin(), mid)); // sort each half
auto right = merge_sort(std::vector<int>(mid, a.end()));
return merge(left, right);
}

Tracing the single merge step on left = [1, 5] and right = [3, 8], element by element:
| Step | i, j | Compare | Take | Output so far |
|---|---|---|---|---|
| 1 | i=0, j=0 | left[0]=1 vs right[0]=3 | 1 (left) | [1] |
| 2 | i=1, j=0 | left[1]=5 vs right[0]=3 | 3 (right) | [1, 3] |
| 3 | i=1, j=1 | left[1]=5 vs right[1]=8 | 5 (left) | [1, 3, 5] |
| 4 | i=2, j=1 | left exhausted | append remaining right ([8]) | [1, 3, 5, 8] |
Four elements, at most four comparisons (three real comparisons, then one exhaustion check) — a merge
of two runs of total length n never does more than n − 1 comparisons, which is the entire basis for
the -per-level claim below.
Why the complexity is exactly
The recursion halves the input, so it has levels. Every level merges a total of n elements, regardless of how they are distributed across subarrays. Work per level is therefore , and the total is — with no dependence on the data, which is why best, average and worst are all the same.
Stability comes from one character
left[i] <= right[j] takes from the left run when elements compare equal. Since the left run
holds elements that came earlier in the original array, equal elements keep their original order.
Change it to < and the merge takes from the right on ties, silently destroying stability.
Practical Usage
Mergesort is the right choice when:
- Stability is required — sorting by a secondary key after a primary one.
- Worst-case guarantees matter — real-time or adversarial contexts where quicksort's is unacceptable.
- The data does not fit in memory. External mergesort reads sorted runs from disk and merges them with sequential I/O, which is the one access pattern storage is good at. This is how databases sort tables larger than RAM.
- You are sorting a linked list. Merging lists needs only pointer rewiring — extra space, no random access required. This is the one case where mergesort is better on a list than on an array.
- You want to parallelise. The two recursive calls share nothing.
- Python
- C++
# Bottom-up mergesort — no recursion, same complexity
def merge_sort_iterative(a):
width = 1
while width < len(a):
for i in range(0, len(a), 2 * width):
a[i:i + 2 * width] = merge(a[i:i + width], a[i + width:i + 2 * width])
width *= 2
return a
// Bottom-up mergesort — no recursion, same complexity
void merge_sort_iterative(std::vector<int>& a) {
auto n = static_cast<std::ptrdiff_t>(a.size());
for (std::ptrdiff_t width = 1; width < n; width *= 2) {
for (std::ptrdiff_t i = 0; i < n; i += 2 * width) {
auto mid = std::min(i + width, n);
auto end = std::min(i + 2 * width, n);
std::inplace_merge(a.begin() + i, a.begin() + mid, a.begin() + end);
}
}
}
- Python
- C++
# checked on the traced merge and on the full sort
assert merge([1, 5], [3, 8]) == [1, 3, 5, 8]
assert merge_sort([5, 1, 8, 3]) == [1, 3, 5, 8]
assert merge_sort([]) == []
int main() {
assert((merge({1, 5}, {3, 8}) == std::vector<int>{1, 3, 5, 8}));
assert((merge_sort({5, 1, 8, 3}) == std::vector<int>{1, 3, 5, 8}));
}
Edge Cases & Pitfalls
Mergesort cannot merge in place efficiently. Naive in-place merge algorithms exist but are either or have constant factors bad enough to erase the benefit. For a large array this means a second buffer the same size — which can be the deciding factor on memory-constrained systems, and is the main reason quicksort is preferred for in-memory array sorting.
A good implementation allocates one scratch buffer up front and reuses it, rather than allocating per merge as the readable version above does.
- Slicing allocates. The Python above creates new lists at every level — clear, but it does roughly allocation. Production code passes indices into a single shared buffer.
<instead of<=in the merge silently loses stability.- Recursion depth is , which is safe — unlike quicksort's worst case.
Comparisons
| Mergesort | Quicksort | Heapsort | |
|---|---|---|---|
| Worst case | |||
| Space | |||
| Stable | Yes | No | No |
| Locality | Good — sequential merges | Excellent | Poor — jumps around |
| Typical speed on arrays | Good | Fastest | Slowest of the three |
| Linked lists | Ideal | Awkward | Impractical |
Recall
References
- Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, 4th ed., §2.3 — mergesort, and the recurrence-tree analysis of its complexity.
- Knuth, The Art of Computer Programming, Vol. 3, §5.2.4 — merging and external sorting, including multiway merges.
Books & Videos
- VisuAlgo — Sorting — watch the merge levels build up.
Related Pages
- Quicksort — the in-place alternative with a worse worst case.
- Divide & Conquer — the general pattern this instantiates.
- Choosing a Sort — Timsort, which is an adaptive mergesort.