Merge Sort

Merge sort is the first sort in this track that beats O(n²). It does that with a divide-and-conquer strategy: split the array in half, recursively sort each half, then merge the two already-sorted halves back together. The trick that makes it fast is that merging two sorted arrays is cheap — a single linear pass — so all the real work happens in that merge step, and there are only O(log n) levels of splitting.

Divide and Conquer

Three steps, applied recursively:

  • Divide: split the array into two halves at the midpoint.
  • Conquer: recursively merge-sort each half. The base case is an array of length 0 or 1 — already sorted by definition.
  • Combine: merge the two sorted halves into one sorted array.
public static void mergeSort(int[] arr, int left, int right) {
    if (left >= right) {
        return;                       // base case: 0 or 1 element, already sorted
    }
    int mid = left + (right - left) / 2;
    mergeSort(arr, left, mid);          // conquer left half
    mergeSort(arr, mid + 1, right);   // conquer right half
    merge(arr, left, mid, right);       // combine
}

Time complexity: O(log n) levels of recursion. Space complexity: O(log n) recursion stack depth for this call alone — the merge() helper below adds the dominant O(n) auxiliary space.

The merge() Helper, Fully Worked

merge() assumes arr[left..mid] and arr[mid+1..right] are each already sorted, and combines them into one sorted run. It copies both halves into a temporary array, then walks two pointers — one per half — always picking the smaller of the two current elements to write back into the original array next.

private static void merge(int[] arr, int left, int mid, int right) {
    int[] temp = new int[right - left + 1];
    int i = left;         // pointer into left half
    int j = mid + 1;     // pointer into right half
    int k = 0;            // pointer into temp

    while (i <= mid && j <= right) {
        if (arr[i] <= arr[j]) {
            temp[k++] = arr[i++];    // "<=" (not "<") keeps equal elements' original order — this is what makes merge sort stable
        } else {
            temp[k++] = arr[j++];
        }
    }
    while (i <= mid) {
        temp[k++] = arr[i++];        // copy any leftover left-half elements
    }
    while (j <= right) {
        temp[k++] = arr[j++];        // copy any leftover right-half elements
    }
    for (int x = 0; x < temp.length; x++) {
        arr[left + x] = temp[x];     // copy merged result back into the original array
    }
}

Time complexity: O(n) — every element from both halves is visited exactly once across the comparison loop and the two cleanup loops combined. Space complexity: O(n) — the temp array holds every element in this range.

Recursion Tree Visualization

Trace merge sort on [8, 3, 7, 4, 2, 6, 1, 5] (8 elements). The array keeps splitting in half until every piece has 1 element, then merges climb back up:

Level 0 (split): [8,3,7,4,2,6,1,5] Level 1 (split): [8,3,7,4] [2,6,1,5] Level 2 (split): [8,3] [7,4] [2,6] [1,5] Level 3 (base case): [8][3] [7][4] [2][6] [1][5] Level 2 (merge): [3,8] [4,7] [2,6] [1,5] Level 1 (merge): [3,4,7,8] [1,2,5,6] Level 0 (merge): [1,2,3,4,5,6,7,8]
LevelNumber of merge() callsWork per callTotal work at this level
0 (top)1O(n)O(n)
12O(n/2) eachO(n)
24O(n/4) eachO(n)
.........O(n)
log₂(n) (bottom)nO(1) eachO(n)
Deriving O(n log n): there are exactly log₂(n) levels (each level halves the piece size until it reaches 1), and every level does O(n) total work combining its pieces back together — regardless of how many pieces that level has, since the piece sizes shrink proportionally. O(n) work per level × log₂(n) levels = O(n log n) total.

Why O(n log n) Time, O(n) Space

Time complexity: O(n log n) in the best, average, and worst case alike — unlike quick sort (covered next), merge sort's split point is always the midpoint regardless of the data, so there's no unlucky input that makes it degrade. Space complexity: O(n) — the merge() helper allocates a temp array sized to the range being merged, and while these temp arrays don't all exist simultaneously, the largest single allocation (at the top level) is already O(n), plus O(log n) recursion stack depth which is dominated by the O(n) auxiliary array.

Stability

Merge sort is stable: when both halves have equal elements, the merge() code above picks from the left half first (arr[i] <= arr[j], not strictly <), and since the left half always precedes the right half in the original array, equal elements keep their original relative order.

Why It's a Good Default When Stability Matters

Merge sort's guaranteed O(n log n) — no worst-case surprises — combined with stability makes it the right choice whenever you're sorting objects by one field and need ties on that field to preserve an earlier order (e.g. sorting purchase records by date, where same-date records should stay in the order they were entered). This is exactly why Java's Collections.sort() and Arrays.sort() on object arrays use Timsort, a hybrid built on merge sort's merging idea — precisely because sorting objects usually means sorting by a field, where stability is expected.

Next up: Quick Sort gets the same O(n log n) average time without merge sort's O(n) extra space — by partitioning in place around a pivot instead of merging into a temp array. That trade brings a worst case of its own.