Merge Sort

Divides the array in half, sorts each half, then merges them. Guaranteed O(n log n) performance regardless of input.

Best O(n log n) Avg O(n log n) Worst O(n log n) Space O(n) Stable Yes In-place No Comparison-based

How it works

Merge Sort divides the array in half, recursively sorts each half, then merges the two sorted halves into one. The merge step walks both halves with two pointers, always copying the smaller front element next.

The recursion depth is log n and each level does O(n) work to merge, giving a guaranteed O(n log n) regardless of input order.

Implementation

function mergeSort(arr, stats) {
  const n = arr.length;
  if (n < 2) {
    if (n === 1) markSorted(0);
    return;
  }
  const INSERTION_THRESHOLD = 16;
  const aux = new Array(n);
  const metrics = stats.educational.metrics;
  const recordMetric = (name, delta = 1) => {
    metrics[name] = (metrics[name] || 0) + delta;
  };
  metrics.insertionThreshold = INSERTION_THRESHOLD;
  function writeValue(index, value) {
    arr[index] = value;
    recordMetric('writeOperations');
    write(index, value);
  }
  function insertionSort(lo, hi) {
    recordMetric('insertionSortCalls');
    for (let i = lo + 1; i <= hi; i++) {
      const value = arr[i];
      let j = i;
      while (j > lo) {
        compare(j - 1, i);
        if (arr[j - 1] <= value) break;
        writeValue(j, arr[j - 1]);
        j--;
      }
      writeValue(j, value);
    }
  }
  function merge(lo, mid, hi) {
    for (let i = lo; i <= hi; i++) aux[i] = arr[i];
    compare(mid, mid + 1);
    if (aux[mid] <= aux[mid + 1]) {
      recordMetric('mergeSkips');
      return;
    }
    let left = lo;
    let right = mid + 1;
    let dest = lo;
    while (left <= mid && right <= hi) {
      compare(left, right);
      if (aux[left] <= aux[right]) writeValue(dest++, aux[left++]); else writeValue(dest++, aux[right++]);
    }
    while (left <= mid) writeValue(dest++, aux[left++]);
    while (right <= hi) writeValue(dest++, aux[right++]);
    recordMetric('mergeCalls');
    checkpoint(Math.min(metrics.mergeCalls, n), n);
  }
  function sort(lo, hi) {
    if (hi - lo + 1 <= INSERTION_THRESHOLD) {
      insertionSort(lo, hi);
      return;
    }
    const mid = lo + (hi - lo >> 1);
    sort(lo, mid);
    sort(mid + 1, hi);
    merge(lo, mid, hi);
  }
  sort(0, n - 1);
  for (let i = 0; i < n; i++) markSorted(i);
  checkpoint(n, n);
}

Advantages

  • Guaranteed O(n log n) on every input — no pathological cases.
  • Stable, which makes it the standard choice for sorting records by a secondary key.
  • Predictable and easy to parallelize or adapt to external (on-disk) sorting.

Disadvantages

  • Needs O(n) auxiliary memory for the classic implementation.
  • Not in-place, and slower than Quick Sort in practice on small in-memory arrays due to the extra copying.

When to use it

Reach for it when stability matters, when worst-case guarantees matter, or when sorting data larger than RAM via external merge passes.