Lesson 16 of 30
Divide and Conquer, with Merge Sort
Split a problem in half, solve each half recursively, and combine the results — the strategy behind merge sort, and the proof of why it's O(n log n) rather than O(n²).
Divide and conquer is the strategy behind some of the most important
algorithms that exist, and it's really just recursion with a specific shape:
split the problem, solve the pieces the same way, and combine. This lesson
works through merge sort as the clearest possible example — not because
you'll hand-write sorts often (the next lesson covers when the built-in
sort is enough, which is almost always), but because the reasoning that
proves merge sort is O(n log n) is a template you'll reuse for analyzing
other divide-and-conquer code.
The strategy, in three steps
- Divide — split the problem into smaller subproblems of the same kind.
- Conquer — solve each subproblem, typically by applying the same strategy recursively, down to a base case trivial enough to answer directly.
- Combine — merge the subproblems' solutions into the solution for the original, larger problem.
Merge sort, following the recipe exactly
function mergeSort(arr) {
if (arr.length <= 1) return arr; // base case — already "sorted"
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid)); // divide + conquer, left half
const right = mergeSort(arr.slice(mid)); // divide + conquer, right half
return merge(left, right); // combine
}
function merge(left, right) {
const result = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) result.push(left[i++]);
else result.push(right[j++]);
}
// one side may have leftovers once the other is exhausted — append them
while (i < left.length) result.push(left[i++]);
while (j < right.length) result.push(right[j++]);
return result;
}Divide: split the array in half. Conquer: sort each half by calling
mergeSort again (the recursive case) until arrays are down to size 0 or 1
(the base case — a single element is trivially "sorted"). Combine: merge
walks both already-sorted halves with two pointers — the same two-pointer
technique from earlier in this course — picking whichever front element is
smaller, until one side runs out.
Proving it's O(n log n), not O(n²)
This is the part worth internalizing, because the same two-piece argument (how deep does the recursion go, and how much work happens at each level) generalizes to analyzing other divide-and-conquer algorithms you'll meet later.
How deep is the recursion? Every level splits the array exactly in half.
Going from n elements down to arrays of size 1 by repeatedly halving takes
log₂(n) steps — that's the definition of a logarithm. So the recursion is
log(n) levels deep.
How much work happens at each level? At any given level of the
recursion, the subarrays at that level, added together, still contain all
n original elements — they've just been split into more, smaller pieces.
Merging is linear in the combined size of what's being merged, so the total
merging work across one entire level — summed across every merge happening at
that level — is O(n), regardless of how many pieces that level has been split
into.
- Step 1
Level 0 (top)
One array of size n. No merging yet — this is where the splitting starts.
- Step 2
Level 1
Two arrays of size n/2. Merging them back together (later) touches n elements total.
- Step 3
Level 2
Four arrays of size n/4. Merging at this level, across all four, still totals n elements touched.
- Step 4
...down to log(n) levels
Each level's total merge work is O(n) — multiply by log(n) levels: O(n log n) overall.
Multiply: O(n) work per level, across O(log n) levels, gives O(n log
n) total. This is strictly better than the O(n²) sorts (like a naive
bubble or insertion sort) for large n — at a million elements, n log n
is around 20 million operations; n² is a trillion.
The space cost, and why it's a deliberate trade
merge builds a brand-new result array at every call, and slice()
creates new arrays for every split. This makes merge sort O(n) auxiliary
space — not in-place, unlike some O(n log n) alternatives (quicksort,
covered only briefly in the next lesson, is typically in-place but doesn't
have merge sort's worst-case guarantee).
That space cost buys two real properties: a guaranteed O(n log n) even in the worst case (some in-place sorts degrade to O(n²) on specific unlucky inputs), and stability — equal elements keep their original relative order, which matters whenever you're sorting objects by one field but want ties broken by "whichever came first."
What to remember
- Divide and conquer: split into smaller subproblems of the same kind, solve each one recursively, combine the results.
- Merge sort's complexity proof has two parts: the recursion is
log(n)levels deep (repeated halving), and each level's total merge work is O(n) — multiplying gives O(n log n) overall. This two-part argument generalizes to other divide-and-conquer algorithms. - Merge sort trades O(n) auxiliary space (new arrays at every merge) for a guaranteed worst-case O(n log n) and stability — a deliberate trade, not a flaw.
- The
mergestep itself reuses the two-pointer technique from earlier in this course — recognizing that reuse is a preview of how these patterns compose in real, larger algorithms.
Check yourself
4 questions · pass 3/4 to unlock Binary Trees and Tree Vocabulary
1.What are the three steps of the divide-and-conquer strategy, in order?
2.In merge sort, how deep does the recursive splitting go before hitting the base case, for an input of size n?
3.Merging two already-sorted arrays of combined length n takes O(n) time. Given that merge sort does this merge step once at each of its log(n) levels of recursion, and each level's merges together touch all n elements, what is merge sort's overall time complexity?
4.Why is merge sort's O(n) auxiliary space (for the temporary arrays used during merging) considered a real, worthwhile trade rather than a flaw?
4 left to answer