iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Merge Sort

Merge sort splits the input in half, recursively sorts each half, then merges them in linear time. It’s O(n log n) worst case, stable, easy to reason about — the right textbook sort and the basis for external sorting on huge datasets.

Algorithm, complexity, variants, code

EXAMPLE
// 1) The idea
// • Base case: one-element array is sorted
// • Recursive case: sort left half, sort right half, merge them
// • Merge: walk two sorted arrays with two pointers; pick smaller

function mergeSort(arr) {
    if (arr.length <= 1) return arr;
    const mid = Math.floor(arr.length / 2);
    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) {
        if (left[i] <= right[j]) out[k++] = left[i++];   // <= keeps STABLE
        else                       out[k++] = right[j++];
    }
    while (i < left.length)  out[k++] = left[i++];
    while (j < right.length) out[k++] = right[j++];
    return out;
}

// 2) Stability
// '<=' in the merge step keeps elements with equal keys in their original order.
// Stability matters when sorting on a secondary key after a primary sort.

// 3) Complexity
// • Time:    O(n log n) — best, average, worst
// • Space:   O(n) auxiliary (the merge buffer)
// • Stable: yes
// • In-place: no (the textbook version)
// • Adaptive: not really (sorted input still does the work)

// 4) Iterative bottom-up version (no recursion)
function mergeSortIterative(arr) {
    const n = arr.length;
    const buf = new Array(n);
    for (let width = 1; width < n; width *= 2) {
        for (let i = 0; i < n; i += 2 * width) {
            const mid = Math.min(i + width, n);
            const right = Math.min(i + 2 * width, n);
            mergeInPlace(arr, buf, i, mid, right);
        }
    }
    return arr;
}

function mergeInPlace(arr, buf, l, m, r) {
    let i = l, j = m, k = l;
    while (i < m && j < r) buf[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
    while (i < m) buf[k++] = arr[i++];
    while (j < r) buf[k++] = arr[j++];
    for (let x = l; x < r; x++) arr[x] = buf[x];
}

// 5) Sorting linked lists — merge sort is the right answer
// Quicksort needs random access; merge sort works with sequential walks.
function mergeSortList(head) {
    if (!head || !head.next) return head;
    let slow = head, fast = head, prev = null;
    while (fast && fast.next) { prev = slow; slow = slow.next; fast = fast.next.next; }
    prev.next = null;
    const left  = mergeSortList(head);
    const right = mergeSortList(slow);
    return mergeLists(left, right);
}
function mergeLists(a, b) {
    const dummy = { val: 0, next: null };
    let tail = dummy;
    while (a && b) {
        if (a.val <= b.val) { tail.next = a; a = a.next; }
        else                { tail.next = b; b = b.next; }
        tail = tail.next;
    }
    tail.next = a || b;
    return dummy.next;
}

// 6) External sort — too big for memory
// 100 GB file, 4 GB RAM:
//   1. Read 4 GB chunks, sort each in memory, write to disk (25 runs of 4 GB)
//   2. k-way merge those runs using a heap of size k
//   3. Output one final sorted stream
// Used by databases (ORDER BY when work_mem is too small), MapReduce shuffle phase, log indexers.

// 7) Parallel merge sort
// Each recursive call is independent → easy to fork. With p threads:
//   • Time: O(n log n / p)
//   • Work: O(n log n)
// Use thread pools / Web Workers / Goroutines for the recursive split; merge serially.

// 8) Counting inversions — classic interview problem
function countInversions(arr) {
    let count = 0;
    function sort(a) {
        if (a.length <= 1) return a;
        const mid = Math.floor(a.length / 2);
        const L = sort(a.slice(0, mid));
        const R = sort(a.slice(mid));
        let i = 0, j = 0, out = [];
        while (i < L.length && j < R.length) {
            if (L[i] <= R[j]) out.push(L[i++]);
            else { count += L.length - i; out.push(R[j++]); }
        }
        return out.concat(L.slice(i), R.slice(j));
    }
    sort(arr);
    return count;
}

// 9) Comparison to other sorts
//
// Algorithm     Time avg   Time worst   Space   Stable   In-place
// Insertion     O(n²)      O(n²)        O(1)    Yes      Yes        — best on small / nearly sorted
// Quicksort     O(n log n) O(n²)        O(log n) No       Yes        — fast in practice, bad worst case
// Mergesort     O(n log n) O(n log n)   O(n)    Yes      No         — predictable; good for linked lists
// Heapsort      O(n log n) O(n log n)   O(1)    No       Yes        — no extra space; cache-unfriendly
// Timsort       O(n log n) O(n log n)   O(n)    Yes      No         — adaptive; Python/Java sort()
// Radix sort    O(n*k)     O(n*k)       O(n+k)   Depends  No         — integers / fixed-width keys only

// 10) When to choose merge sort
//   ✓ Need a STABLE sort (Java's Arrays.sort for objects uses TimSort = adaptive merge sort)
//   ✓ Sorting linked lists
//   ✓ External sorting on big data
//   ✓ Predictable worst case (real-time systems)
//   ✗ Memory-constrained environments (heapsort or in-place insertion may fit better)
//   ✗ Already-sorted data where adaptive Timsort would skip work

// 11) Real-world stdlib implementations
//   • Python sorted() / list.sort() — TimSort (adaptive merge sort) since 2.3
//   • Java Arrays.sort for OBJECTS — TimSort
//   • Java Arrays.sort for PRIMITIVES — Dual-Pivot Quicksort
//   • JavaScript Array.prototype.sort — ES2019 mandates STABLE; V8 uses TimSort
//   • Rust slice::sort_unstable — pattern-defeating quicksort; sort = merge-based stable
//   • Go sort.Slice — pattern-defeating quicksort (unstable); SortStable = merge sort

// 12) Implementing in-place (advanced)
// True in-place merge sort exists but is complex (Practical In-Place Merge Sort, block merge).
// In practice the O(n) buffer version is simpler and almost always fine.

// 13) Common bugs
// • Using < instead of <= in merge → unstable sort (silently breaks downstream code)
// • Allocating buf inside the recursion → O(n log n) allocations; allocate once at the top
// • Off-by-one in bottom-up bounds — test with arrays of size 1, 2, 3, prime sizes
// • Slicing arr.slice(0, mid) on every recursion → quadratic memory pressure; use indices
// • Stack overflow on huge inputs (deeply recursive) — switch to iterative for n > 10⁶
// • Comparing objects with default < → 'undefined behaviour' for non-primitives in JS; provide a comparator

Why it matters

Merge sort is the textbook stable O(n log n) sort with predictable worst-case — perfect for linked lists, external sorting, and any time you care about stability. For huge inputs, switch to the iterative bottom-up version and allocate the merge buffer once; in production, your language’s stdlib sort is usually merge-based (TimSort) and already great.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
function mergeSort(a) {
    if (a.length < 2) return a;
    const m = a.length >> 1;
    return merge(mergeSort(a.slice(0, m)), mergeSort(a.slice(m)));
}
function merge(L, R) {
    const out = []; let i = 0, j = 0;
    while (i < L.length && j < R.length) out.push(L[i] <= R[j] ? L[i++] : R[j++]);
    return out.concat(L.slice(i)).concat(R.slice(j));
}
Try it Yourself »

Discussion

Loading…