Merge Sort
Learn how merge sort uses a divide-and-conquer strategy and recursion to break an array down into single elements and merge them back in O(n log n) time.
Introduction
Merge sort is an efficient, comparison-based sorting algorithm that uses a divide-and-conquer approach. It splits an unsorted array into smaller halves until each piece has only zero or one element, and then merges those pieces back together in sorted order.
The Core Idea
An array with 0 or 1 element is already sorted by definition. Merge sort exploits this by repeatedly splitting an array in half until all sub-arrays have 1 element, then merging them back together in sorted order.
Why Merge Sort?
Elementary sorting algorithms (bubble sort, selection sort, and insertion sort) run in quadratic time. While they perform well on tiny arrays, they struggle as data grows:
- Sorting an array of 100,000 random items with bubble sort can take 20 seconds or more.
- Sorting that same array with merge sort takes a fraction of a second.
Merge sort cuts the time complexity from down to , making it capable of handling very large datasets efficiently.
How Merge Sort Works
Merge sort combines three steps: split, sort, and merge.
- Divide: Split the array in half repeatedly until every sub-array has only 1 element (or 0).
- Conquer: Since a 1-element array is already sorted, begin merging adjacent pairs.
- Combine: Merge smaller sorted arrays into larger sorted arrays until one complete array remains.
Part 1: Merging Two Sorted Arrays
Before writing the main recursive sort, we need a helper function that takes two already-sorted arrays and merges them into a single sorted array.
Merging logic
- Create an empty
resultsarray. - Use two pointers (
iandj), both starting at index0. - While both arrays still have elements to look at:
- Compare
arr1[i]witharr2[j]. - Push the smaller value into
resultsand move that pointer forward by1.
- Compare
- Once one array runs out of elements, push all remaining elements from the other array into
results. - Return
results.
Implementation
const merge = (arr1, arr2) => {
const results = [];
let i = 0;
let j = 0;
// Compare elements from both arrays and push the smaller one
while (i < arr1.length && j < arr2.length) {
if (arr1[i] < arr2[j]) {
results.push(arr1[i]);
i++;
} else {
results.push(arr2[j]);
j++;
}
}
// Add remaining elements from arr1 (if any)
while (i < arr1.length) {
results.push(arr1[i]);
i++;
}
// Add remaining elements from arr2 (if any)
while (j < arr2.length) {
results.push(arr2[j]);
j++;
}
return results;
};Part 2: The Full Merge Sort Algorithm
With our merge helper in place, the recursive sorting function is surprisingly short:
- Base Case: If the array length is
0or1, it is already sorted. Return the array. - Find the Midpoint: Use
Math.floor(array.length / 2). - Split and Recurse:
- Call
mergeSorton the left half (array.slice(0, mid)). - Call
mergeSorton the right half (array.slice(mid)).
- Call
- Merge: Return
merge(left, right).
Implementation
const mergeSort = (array) => {
// Base case: arrays with 0 or 1 element are already sorted
if (array.length <= 1) return array;
const mid = Math.floor(array.length / 2);
// Recursively sort the left and right halves
const left = mergeSort(array.slice(0, mid));
const right = mergeSort(array.slice(mid));
// Merge the sorted halves together
return merge(left, right);
};Examples
Basic usage
const numbers = [10, 24, 76, 73];
console.log(mergeSort(numbers)); // [10, 24, 73, 76]
const dataset = [8, 3, 5, 4, 7, 6, 1, 2];
console.log(mergeSort(dataset)); // [1, 2, 3, 4, 5, 6, 7, 8]Evaluating Merge Sort
Complexity analysis
| Case | Time Complexity | Why |
|---|---|---|
| Best Case | O(n log n) | Even if already sorted, the array is split down to single items and merged back. |
| Average Case | O(n log n) | The number of split levels is always , with work per level. |
| Worst Case | O(n log n) | Reverse-ordered or completely random input takes the exact same number of operations. |
| Space Complexity | O(n) | New sub-arrays are created in memory during splitting and merging. |
Where does come from?
The time complexity is the combination of two operations:
-
The splits ( levels): Every time we split an array in half, we divide the size by 2.
- For an 8-element array: (3 splits, because ).
- For a 32-element array: (5 splits, because ).
- The number of split levels grows logarithmically: .
-
The merge work ( per level): At each level of the tree, merging all the sub-arrays back together requires comparing and placing every single element once: work per level.
Multiplying the levels by the work per level gives a total time complexity of .
The Space Trade-off
Unlike bubble sort, selection sort, and insertion sort (which sort in-place using extra space), merge sort requires auxiliary memory to store the smaller arrays while splitting and merging.
Key Takeaways
- Divide and Conquer: Merge sort recursively breaks an array into smaller sub-arrays until reaching 1-element arrays, then merges them back in order.
- Two Distinct Parts: The algorithm pairs a linear merging helper (
merge, ) with recursive splitting (mergeSort, levels). - Consistent Performance: Merge sort guarantees time across best, average, and worst cases, regardless of how the input data is initially arranged.
- Space Trade-off: Its speed comes at the cost of additional memory to hold sub-arrays during sorting.