Merge Sorting
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an array arr of n integers. Sort it in non-decreasing order using merge sort: split the array into halves, sort each half recursively, then merge the two sorted halves into one sorted array.
Constraints: 1 ≤ n ≤ 10^5, values fit in a 32-bit signed integer. Duplicates and negative numbers are allowed. With n this large, the quadratic sorts are out; merge sort’s O(n log n) is about 1.7 × 10^6 steps.
Example 1
arr = [3, 1, 2, 4, 1, 5, 2, 6, 4]
answer = [1, 1, 2, 2, 3, 4, 4, 5, 6] // duplicates are kept
Example 2
arr = [4, 2, 1, 3]
answer = [1, 2, 3, 4] // halves [4, 2] and [1, 3] become [2, 4] and [1, 3], then merge
Approach 1: Divide, copy, merge
Intuition. Merge sort is built on one observation: merging two sorted arrays is easy and linear. Put a finger on the front of each; the smaller of the two fingered elements is the smallest of everything that is left, so output it and advance that finger. Each step consumes one element, so merging a and b costs |a| + |b| steps.
Given a linear merge, sorting becomes divide and conquer: cut the array in half, sort both halves (recursively, by the same procedure), and merge. The recursion stops when a piece has fewer than two elements, which is trivially sorted. The most direct implementation copies the halves into their own arrays, sorts those, and merges them back into the original.
Algorithm.
- If
arrhas fewer than two elements, return. - Copy
arr[0..mid-1]toleftandarr[mid..n-1]toright, withmid = n / 2. - Recursively sort
leftandright. - Merge: while both have elements, copy the smaller front element into
arr(taking fromlefton ties); then copy whatever remains of either half.
static void mergeSortCopying(int[] arr) {
if (arr.length < 2) return;
int mid = arr.length / 2;
int[] left = Arrays.copyOfRange(arr, 0, mid);
int[] right = Arrays.copyOfRange(arr, mid, arr.length);
mergeSortCopying(left);
mergeSortCopying(right);
int i = 0, j = 0, k = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) arr[k++] = left[i++]; else arr[k++] = right[j++];
}
while (i < left.length) arr[k++] = left[i++];
while (j < right.length) arr[k++] = right[j++];
}
Complexity. Time O(n log n): the recursion has log n levels and the merges on each level touch every element once, so T(n) = 2T(n/2) + O(n). Space O(n) live memory at any moment (the halves along one root-to-leaf path sum to less than 2n) plus O(log n) recursion stack, but a total of O(n log n) ints are allocated over the run, which is what makes this version slower than the next one.
Approach 2: Index ranges and one shared buffer
Intuition. Copying halves is convenient but unnecessary. The recursion can work on index ranges [lo, hi] of the same array; the only place that needs scratch space is the merge, and one buffer of size n, allocated once and shared by every call, is enough. The merge writes the merged run into tmp[lo..hi] and copies it back. Nothing else changes.
Algorithm.
mergeSort(arr)allocatestmpand calls the recursive version on[0, n - 1].mergeSort(arr, tmp, lo, hi): iflo >= hireturn; computemid, recurse on[lo, mid]and[mid + 1, hi], then merge.merge: walkiover[lo, mid]andjover[mid + 1, hi], writing the smaller element totmp[k]; copy leftovers; copytmp[lo..hi]back intoarr.
static void mergeSort(int[] arr) {
if (arr.length < 2) return;
int[] tmp = new int[arr.length];
mergeSort(arr, tmp, 0, arr.length - 1);
}
static void mergeSort(int[] arr, int[] tmp, int lo, int hi) {
if (lo >= hi) return;
int mid = lo + (hi - lo) / 2;
mergeSort(arr, tmp, lo, mid);
mergeSort(arr, tmp, mid + 1, hi);
merge(arr, tmp, lo, mid, hi);
}
static void merge(int[] arr, int[] tmp, int lo, int mid, int hi) {
int i = lo, j = mid + 1, k = lo; // left run arr[lo..mid], right run arr[mid+1..hi]
while (i <= mid && j <= hi) {
if (arr[i] <= arr[j]) tmp[k++] = arr[i++]; else tmp[k++] = arr[j++];
}
while (i <= mid) tmp[k++] = arr[i++];
while (j <= hi) tmp[k++] = arr[j++];
for (k = lo; k <= hi; k++) arr[k] = tmp[k];
}
Dry run of the final merge in Example 2, with left run [2, 4] and right run [1, 3]:
| arr[i] | arr[j] | arr[i] <= arr[j]? | written to tmp | i | j |
|---|---|---|---|---|---|
| 2 | 1 | no | 1 | 0 | 1 |
| 2 | 3 | yes | 2 | 1 | 1 |
| 4 | 3 | no | 3 | 1 | 2 (right exhausted) |
| 4 | – | left leftovers | 4 | 2 | 2 |
tmp[0..3] = [1, 2, 3, 4] is copied back. The recursion tree for [4, 2, 1, 3] is: [4, 2] → [4], [2] → [2, 4]; [1, 3] → [1], [3] → [1, 3]; then this merge.
Complexity. Time O(n log n) as before, with a much smaller constant because no arrays are created during the recursion. Space O(n) for tmp plus O(log n) stack frames.
The tie rule arr[i] <= arr[j] is what makes merge sort stable: when both runs hold equal elements, the one from the left run (which was earlier in the original array) is written first. Change it to < and equal elements from the right run would jump ahead.
Approach 3: Bottom-up, no recursion
Intuition. Look at the recursion tree from the leaves up. At the bottom every element is a run of width 1. The level above merges adjacent runs into width 2, the next into width 4, and so on. Nothing forces us to arrive there by recursion: we can iterate the widths directly, 1, 2, 4, ..., and for each width merge the pairs of adjacent runs left to right. The same merge routine does the work. This removes the call stack entirely and is the form used for sorting linked lists and external data.
Algorithm.
- If
n < 2return; allocatetmp. - For
width = 1, 2, 4, ...whilewidth < n: - For
lo = 0, 2·width, 4·width, ...while a right run exists (lo < n - width): setmid = lo + width - 1,hi = min(lo + 2·width - 1, n - 1), and merge[lo, mid]with[mid + 1, hi].
static void mergeSortBottomUp(int[] arr) {
int n = arr.length;
if (n < 2) return;
int[] tmp = new int[n];
for (int width = 1; width < n; width *= 2) {
for (int lo = 0; lo < n - width; lo += 2 * width) {
int mid = lo + width - 1;
int hi = Math.min(lo + 2 * width - 1, n - 1);
merge(arr, tmp, lo, mid, hi);
}
}
}
Complexity. Time O(n log n): ⌈log2 n⌉ widths, each merging every element at most once. Space O(n) for the buffer and O(1) stack.
Two details carry the correctness. The condition lo < n - width guarantees mid + 1 = lo + width ≤ n - 1, so the right run is non-empty; a leftover run with no partner is simply left alone and picked up at the next width. And hi is clamped to n - 1 because the last right run may be shorter than width when n is not a power of two. The tests include n = 12345 for exactly this reason.
Edge cases and pitfalls
- Empty or single-element array. Both are returned immediately; a range with
lo >= hiis a base case. No special handling elsewhere. - Computing
mid.lo + (hi - lo) / 2cannot overflow;(lo + hi) / 2can when indices approach2^31. Forn ≤ 10^5both work, but prefer the safe form. - Right run starts at
mid + 1, notmid. Off-by-one here either duplicatesarr[mid]or loses it. Pair it consistently: left[lo, mid], right[mid + 1, hi]. - Copying only
[lo, hi]back. The merge must copy exactly the range it merged; copying the wholetmpback would overwrite parts ofarrthat other ranges have already sorted (or thattmphas never been written to). - Stability requires
<=. Taking from the left run on ties preserves the original order of equal elements. It also costs nothing. - Allocating in the recursion. Creating
left/rightarrays (approach 1) or a newtmpinsidemergeworks but allocatesO(n log n)memory in total and can double the running time. Allocate once. - Skipping unnecessary merges. If
arr[mid] <= arr[mid + 1]the two runs are already in order and the merge can be skipped. It turns sorted input into anO(n)best case at the cost of one comparison per merge. - Recursion depth is only
O(log n), about 17 forn = 10^5, so stack overflow is not a concern, unlike the recursive quadratic sorts.
Related problems
- Inversion of Array (Pre-req: Merge Sort) and Count Inversions count, during the merge, how many right-run elements jump ahead of left-run elements.
- Reverse Pairs is the same merge-time counting with a stricter condition.
- Merge two sorted arrays without extra space is the merge step under a harder memory constraint.
- Quick Sorting is the other
O(n log n)divide-and-conquer sort; it does the work before the recursive calls instead of after. - Insertion Sorting is what library implementations fall back to for tiny ranges inside merge sort.