Merge Sort
Merge sort is built on one small observation: merging two already-sorted lists into one sorted list is easy and fast. So it splits the array in half, sorts each half (by splitting again, and again), and then merges the halves back together. It is guaranteed O(n log n) - no bad inputs, no luck involved - and it is stable. The price: it needs extra memory for the merging.
Watch it sort
Press play - or drag the timeline and step through it yourself.18 values. Every single value is a tiny sorted run. Merge runs in pairs until one run is left.
Complexity
Two sorted piles of exam papers
Two teachers each sorted half of the exam papers by grade. Now you want one sorted pile. Easy: look at the top paper of each pile, take the smaller one, repeat. You never dig inside a pile - the answer is always on top. That merge step is cheap. Merge sort's whole idea is: keep splitting the papers until every pile has one sheet (a pile of one is sorted by definition), then merge the piles back in pairs.
How it works, step by step
Split the array into two halves. Keep splitting each half until every piece has just one value - a single value is already sorted.
Merge pieces back together in pairs: compare the first value of each piece, take the smaller one, repeat until both pieces are empty.
Each merged pair is a sorted run twice as long as before.
Keep merging: runs of 1 become runs of 2, then 4, then 8...
After about log n rounds, one run covers the whole array - and it is sorted.
Every round touches each value once, so the total work is n × log n.
The code, in JavaScript
The classic top-down version: split, recurse, merge. The merge helper does all the real work.
function mergeSort(arr) {
if (arr.length <= 1) return arr; // one value = already sorted
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 = [];
let i = 0, j = 0;
// Take the smaller head, again and again.
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) out.push(left[i++]);
else out.push(right[j++]);
}
// One side ran out - the rest of the other side is already sorted.
return [...out, ...left.slice(i), ...right.slice(j)];
}
mergeSort([5, 1, 4, 2]); // → [1, 2, 4, 5]Dry run: sorting [5, 1, 4, 2]
The same steps the visualizer plays, written as a table. Generated by running the real algorithm - not written by hand.
| Step | Pass | Array | What happened |
|---|---|---|---|
| 1 | 1 | [5, 1, 4, 2] | 1 < 5 - take 1 from the right run. |
| 2 | 1 | [1, 1, 4, 2] | Write 1 into slot 1. |
| 3 | 1 | [1, 5, 4, 2] | Write 5 into slot 2. |
| 4 | 1 | [1, 5, 4, 2] | 2 < 4 - take 2 from the right run. |
| 5 | 1 | [1, 5, 2, 2] | Write 2 into slot 3. |
| 6 | 1 | [1, 5, 2, 4] | Write 4 into slot 4. |
| 7 | 2 | [1, 5, 2, 4] | 1 ≤ 2 - take 1 from the left run. |
| 8 | 2 | [1, 5, 2, 4] | 2 < 5 - take 2 from the right run. |
| 9 | 2 | [1, 5, 2, 4] | 4 < 5 - take 4 from the right run. |
| 10 | 2 | [1, 5, 2, 4] | Write 1 into slot 1. |
| 11 | 2 | [1, 2, 2, 4] | Write 2 into slot 2. |
| 12 | 2 | [1, 2, 4, 4] | Write 4 into slot 3. |
| 13 | 2 | [1, 2, 4, 5] | Write 5 into slot 4. |
Good choice when…
- You need a guaranteed O(n log n) - no input can make merge sort slow, unlike quick sort.
- You need a stable sort: sorting a table by one column must not scramble an earlier sort by another column.
- The data does not fit in memory. Merge sort is the base of external sorting - merge sorted chunks from disk, stream-style.
- You are sorting linked lists - merging lists needs no extra array at all, and merge sort becomes the natural choice.
Bad choice when…
- Memory is tight. The O(n) temp array is real - sorting a 1 GB array needs another 1 GB during the merge.
- The array is small. The recursion and copying overhead lose to insertion sort below ~20 items (good implementations switch automatically).
- You just need a quick in-place sort and stability does not matter - quick sort or heap sort skip the extra memory.
Common mistakes
- The merge must use `<=`, not `<`, when taking from the left run. With `<`, equal values from the right run jump ahead - and the sort silently stops being stable.
- Forgetting the leftover copy (`...left.slice(i), ...right.slice(j)`) is the classic bug: the code works on most inputs and quietly drops values when one run empties early.
- `arr.slice(mid)` everywhere makes many copies. Fine for learning; production versions merge inside one shared temp buffer instead of allocating per call.
- Merge sort's O(n log n) is a worst-case guarantee, but its best case is also O(n log n) - on nearly-sorted data, insertion sort or TimSort (which detects sorted runs) is faster.
Merge Sort vs. its closest relatives
| Algorithm | Best | Average | Worst | Space | Stable |
|---|---|---|---|---|---|
| Merge Sortthis page | O(n log n) | O(n log n) | O(n log n) | O(n) | yes |
| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) | no |
| Heap Sort | O(n log n) | O(n log n) | O(n log n) | O(1) | no |
| Insertion Sort | O(n) | O(n²) | O(n²) | O(1) | yes |
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) | yes |