Skip to main content

Mergesort

Overviewโ€‹

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 O(n) per level. Hence O(n log n), on every input, with no worst case to worry about.

A diagram of mergesort on the list 38, 27, 43, 3, 9, 82, 10: red arrows split it down to single elements, then green arrows merge pairs back upward into progressively longer sorted runs
Splitting down (red) does no comparisons at all. All the work is in merging back up (green), where each level touches every element exactly once. Wikimedia Commons, Public domain

Core Conceptsโ€‹

PropertyValue
Best caseO(n log n)
AverageO(n log n)
Worst caseO(n log n) โ€” guaranteed
SpaceO(n) โ€” the merge buffer
StableYes
AdaptiveNo, in the classic form (but see Timsort)
ParallelisesWell โ€” the two halves are independent

Architecture / Mechanismโ€‹

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
Animation of mergesort on a set of bars, showing adjacent runs being merged into progressively longer sorted runs until the whole array is ordered
Runs double in length at each level: 1, 2, 4, 8โ€ฆ The array is sorted after logโ‚‚ n merge passes. Wikimedia Commons, CC BY-SA 3.0

Why the complexity is exactly O(n log n)โ€‹

The recursion halves the input, so it has logโ‚‚ n levels. Every level merges a total of n elements, regardless of how they are distributed across subarrays. Work per level is therefore ฮ˜(n), and the total is ฮ˜(n log n) โ€” 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 O(nยฒ) 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 โ€” O(1) 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.
# 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

Edge Cases & Pitfallsโ€‹

The O(n) space is the real cost

Mergesort cannot merge in place efficiently. Naive in-place merge algorithms exist but are either O(nยฒ) 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 O(n log n) allocation. Production code passes indices into a single shared buffer.
  • < instead of <= in the merge silently loses stability.
  • Recursion depth is O(log n), which is safe โ€” unlike quicksort's worst case.

Comparisonsโ€‹

MergesortQuicksortHeapsort
Worst caseO(n log n)O(nยฒ)O(n log n)
SpaceO(n)O(log n)O(1)
StableYesNoNo
LocalityGood โ€” sequential mergesExcellentPoor โ€” jumps around
Typical speed on arraysGoodFastestSlowest of the three
Linked listsIdealAwkwardImpractical

Referencesโ€‹

  • Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, ยง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โ€‹