Algorithms

Merge Sort

Divide, conquer, then merge

Merge sort is a divide-and-conquer sorting algorithm that recursively splits an array in half, sorts each half, then merges the sorted halves back together. It runs in O(n log n) time on every input — best case, average case, worst case — at the cost of O(n) extra memory.

  • Time (avg, worst, best)O(n log n)
  • SpaceO(n)
  • StableYes
  • In-placeNo
  • Best forLinked lists, external sorts, predictable performance

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

How merge sort works

Merge sort breaks a hard problem into easy ones, then stitches the answers back together. The hard problem is "sort this array." The easy problem is "merge two already-sorted arrays into one sorted array" — which can be done in linear time with two pointers.

The algorithm does this recursively:

  1. Divide. Split the array in half. If it has one element or fewer, it's already sorted — return it.
  2. Conquer. Recursively sort each half.
  3. Merge. Walk through both sorted halves simultaneously with two pointers, picking the smaller front element each step and writing it to the output.

The recursion bottoms out at single-element arrays (which are trivially sorted), then unwinds back up. At each level, every element gets touched exactly once during a merge, so each level is O(n). With log₂(n) levels, the total cost is O(n log n).

When to use merge sort

  • You need predictable performance. Quicksort is faster on average, but its worst case is O(n²). Merge sort guarantees O(n log n) on every input — there are no pathological cases.
  • You need a stable sort. Merge sort preserves the relative order of equal elements, which matters when you're sorting records by multiple keys.
  • You're sorting a linked list. No random access? No problem — merge sort doesn't need it. The split is just a pointer manipulation.
  • The data doesn't fit in memory. Merge sort is the foundation of external sorting algorithms used by databases and Hadoop-era big data systems.

The trade-off is the O(n) auxiliary memory. If you're tight on RAM, a heap sort or in-place quicksort variant might be better.

Merge sort vs quicksort

Merge sortQuicksort
Time (average)O(n log n)O(n log n)
Time (worst)O(n log n)O(n²)
SpaceO(n)O(log n)
StableYesNot by default
In-placeNoYes
Cache behaviorPoor (writes scattered)Excellent (in-place)

In practice, quicksort is usually 2-3× faster than merge sort on random data because of cache-friendliness — but merge sort wins on guarantees. Many language standard libraries (Python, Java for objects, Rust) use hybrid algorithms like Timsort or pdqsort that combine the best of both.

Pseudo-code

function mergeSort(arr):
    if arr.length <= 1:
        return arr
    mid = floor(arr.length / 2)
    left  = mergeSort(arr[0:mid])
    right = mergeSort(arr[mid:])
    return merge(left, right)

function merge(left, right):
    result = []
    i = 0; j = 0
    while i < left.length and j < right.length:
        if left[i] <= right[j]:
            result.push(left[i]); i++
        else:
            result.push(right[j]); j++
    while i < left.length: result.push(left[i++])
    while j < right.length: result.push(right[j++])
    return result

Note left[i] <= right[j] — using <= rather than < is what makes the sort stable. Equal elements from the left half always emplace first.

JavaScript implementation

function mergeSort(arr) {
  if (arr.length <= 1) return arr;
  const mid = arr.length >> 1;
  const left  = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  return merge(left, right);
}

function merge(left, right) {
  const out = new Array(left.length + right.length);
  let i = 0, j = 0, k = 0;
  while (i < left.length && j < right.length) {
    out[k++] = left[i] <= right[j] ? left[i++] : right[j++];
  }
  while (i < left.length)  out[k++] = left[i++];
  while (j < right.length) out[k++] = right[j++];
  return out;
}

Python implementation

def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left  = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)

def merge(left, right):
    out = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            out.append(left[i]); i += 1
        else:
            out.append(right[j]); j += 1
    out.extend(left[i:])
    out.extend(right[j:])
    return out

Python's built-in sorted() and list.sort() use Timsort — a merge sort variant that detects already-sorted runs and exploits them. On real-world data with structure (which is most data), Timsort approaches O(n) instead of O(n log n).

Optimizations worth knowing

  • Switch to insertion sort for small subarrays (typically n < 16). Insertion sort has lower constant factors and dominates merge sort for tiny inputs. Standard libraries do this.
  • Top-down vs bottom-up. The version above is top-down (recursive). Bottom-up merge sort is iterative — start by merging pairs of single elements, then pairs of 2-element runs, then pairs of 4-element runs. Same complexity, no recursion overhead.
  • Reuse a single auxiliary buffer. Allocating a new temp array per merge is wasteful. Better implementations alternate between two buffers, halving allocation cost.
  • Detect already-sorted halves. If left[last] <= right[0], no merge is needed — just concatenate. Timsort takes this much further.

Frequently asked questions

Why is merge sort O(n log n)?

At each recursion level, the algorithm does O(n) work to merge the two halves. There are log₂(n) levels because the array halves each time. Multiply them and you get O(n log n) — and this holds regardless of the input order, unlike quicksort.

Is merge sort stable?

Yes, when implemented correctly. During the merge step, if two elements compare equal, the one from the left half is emplaced first — preserving the original relative order. That's why merge sort is the default for sorting records by multiple keys.

Why does merge sort use O(n) extra memory?

The merge step needs a temporary array to hold the merged result before writing back. There are in-place merge-sort variants, but they're significantly slower in practice and rarely worth the complexity.

When should I prefer merge sort over quicksort?

When you need a guaranteed O(n log n) worst case (e.g., real-time systems), when you need stability, when sorting linked lists (no random access — merge sort is naturally suited), or when sorting data that doesn't fit in memory (external sort).

How does merge sort work on linked lists?

Beautifully — it's actually faster than on arrays for linked lists because the split step is O(1) (just relink the middle pointer) and the merge step doesn't need any extra memory beyond pointer manipulation. Java's LinkedList sort uses merge sort for this reason.