Divide and Conquer & Mergesort
Divide and conquer breaks a problem into smaller copies of itself, solves them recursively, and stitches the answers together. We meet the paradigm through mergesort — its merge step, its loop-invariant proof, and the recursion tree that pins its cost at — then count inversions with the same machinery and distill the whole pattern into the master theorem.
╌╌╌╌
Some problems are easiest to solve by reducing them to smaller versions of themselves. This is the divide-and-conquer paradigm, and it is one of the most productive ideas in all of algorithm design. Every divide-and-conquer algorithm has the same three-part skeleton:
- Divide the problem into one or more subproblems that are smaller instances of the same problem.
- Conquer the subproblems by solving them recursively. When a subproblem is small enough (the base case), solve it directly without recursing.
- Combine the subproblem solutions into a solution for the original problem.
Erickson's advice captures the mindset: assume the recursion already
works, so that the recursive calls correctly solve the smaller instances, and
focus your energy on the divide and combine steps. This recursion fairy
1
stance turns a single hard problem into two manageable questions: how do I split? and
how do I merge?
The payoff is always a recurrence. If an instance of size spawns subproblems each of size , and the divide-plus-combine work costs , then the total cost obeys
Almost every algorithm in this module is an exercise in choosing , , and wisely and then reading off . The master theorem (stated at the end of this lesson) turns that reading-off into a mechanical three-case rule; the recursion tree is the picture behind it. Mergesort is the cleanest first example, so we start there.
The sorting problem, revisited
Recall the specification from the previous module:
Insertion sort grew a sorted prefix one element at a time, costing in the worst case. Divide and conquer does much better. Ask: if I already had two sorted halves, could I finish the job cheaply? The answer, yes, by merging, gives us mergesort.2
Mergesort
To sort the subarray , split it at the midpoint , recursively sort the two halves, and merge them back together. A single element () is already sorted, so it is the base case.
- 1if then
- 2split point
- 3callsort left half
- 4callsort right half
- 5callcombine halves
All the real work lives in the combine step. takes two adjacent sorted runs, and , and interleaves them into a single sorted run in place. It copies each half into a scratch array, then repeatedly takes the smaller of the two front elements and writes it back.
- 1
- 2
- 3let and be new arrays
- 4for to do
- 5copy left half
- 6for to do
- 7copy right half
- 8sentinel guards the run end
- 9
- 10
- 11
- 12for to do
- 13if then
- 14
- 15
- 16else
- 17
- 18
The two sentinel values are a small but useful device: once one
half is used up, its front element is forever , so the comparison
always picks from the other half. This removes the need to test have we run out?
on every iteration.
Picture the merge in flight. Two sorted runs and sit above the output; the cursors and point at their smallest uncopied elements, and marks where the next winner lands in . Each step compares to , writes the smaller, and advances that one cursor.
Here and , so wins: it is written to and advances. The two sentinels guard the right ends so the comparison is always well-defined.
Why merge is correct
Merge runs in time on elements: each of the iterations of the final for loop does work and advances exactly one of , . Correctness rests on a loop invariant:
For example, run the loop to completion on the two halves and . After the copy phase, and . Each row below is one iteration of the for loop: one comparison, one write, one cursor advance.
| comparison | winner | after the write | |
|---|---|---|---|
| vs | |||
| vs | (tie: takes left) | ||
| vs | |||
| vs | |||
| vs | |||
| vs | |||
| vs | |||
| vs |
Two rows deserve a second look. At the fronts tie at ; the comparison takes from , the left half — the choice that makes the sort stable (more on this below). At the right run is exhausted and its front is the sentinel , so the comparison automatically drains the rest of with no special end-of-run test. Eight iterations, eight writes, each element landing in its sorted slot:
Analyzing the cost
Let be the worst-case running time of mergesort on elements. Splitting costs , the two recursive calls cost , and the merge costs . So
To see why this resolves to , draw the recursion tree. Each node is labeled with the non-recursive work it does, the cost of its own merge. The root merges elements; its two children each merge ; the next level has four nodes each merging ; and so on.
Each level sums to the same amount. The root level is ; the next is ; the next is ; in general level has nodes each doing work, for a row total of .
Halving from down to the base case of takes steps, so there are levels. Multiplying the per-level cost by the number of levels:
This is the canonical application of the master theorem (, , , so and we land in the balanced case), but the recursion tree makes the concrete: levels, work apiece.
Stability
This falls out of the in : when we take from , the left (earlier) half, first. We saw it happen in the trace above, at : the two front elements tied at , and the left half's copy was emitted first. Since every element of came from earlier positions in than every element of , and recursion preserves the property inductively, equal elements never swap places.
Stability matters when records are sorted on one key but carry others: a stable sort lets you sort by secondary key, then primary key, and trust that ties on the primary preserve the secondary ordering. Sorting employees by department after sorting them by name leaves each department's roster alphabetized — but only if the second sort is stable.
Mergesort versus other sorts
| Property | Mergesort | Insertion sort | Heapsort | Quicksort |
|---|---|---|---|---|
| Worst case | ||||
| Average case | ||||
| Extra space | ||||
| Stable | yes | yes | no | no |
| In place | no | yes | yes | yes |
Mergesort's worst-case guarantee and stability make it the sort of choice when predictability matters or when data does not fit in memory. Its sequential, merge-based access pattern is ideal for sorting linked lists and for external sorting of data streamed from disk.3 Its cost is the auxiliary array. Quicksort, the subject of the next lesson, trades that guarantee for better constants and in-place operation.
When the recursion is not worth it
Divide and conquer wins asymptotically, but each recursive call carries real overhead: stack frames, index arithmetic, the scratch-array traffic of . On a subarray of ten elements, insertion sort's tight loop with no allocation beats all of that machinery outright. Two standard adjustments exploit this.
Cut off to insertion sort. Stop recursing once the subarray shrinks below a threshold and finish it with insertion sort. The base cases cost each, for total, while the merging now spans only levels of work apiece:
For constant this is still — the asymptotics are untouched — but the constant factor drops because the bottom levels of the recursion tree, the levels with the most nodes and the most per-call overhead, are replaced by a handful of cheap quadratic sorts. In practice is tuned somewhere between and .
Go bottom-up. The recursion can be removed entirely. Bottom-up mergesort treats the array as sorted runs of width , then makes passes that merge adjacent runs pairwise: after the first pass the runs have width , then , then , doubling until one run remains.
Each pass is a plain loop over the array doing merge work, and there are passes, so the cost is the same — the recursion tree read bottom-to-top instead of top-to-bottom. What iteration buys is engineering: no stack, no function-call overhead, and a shape that suits linked lists (splice runs instead of copying) and external sorting, where each pass is one sequential sweep over the data on disk. What it gives up is the cutoff trick's easy hybridization and any chance to exploit runs that are already sorted — refinements that top-down and bottom-up variants alike can bolt back on.
The broader moral: divide and conquer sets the asymptotic ceiling, but at small sizes a simple iterative method with better constants wins, so real implementations are hybrids — recursion (or doubling passes) for the large scales, iteration for the base.
Counting inversions
Here is a problem that has nothing to do with sorting on its surface, yet falls to the very machinery we just built. Given a list , how close to sorted is it? A natural measure counts the pairs that are out of order.
A sorted array has zero inversions; a reverse-sorted one has the maximum,
. (Inversion counts also drive collaborative-filtering how similar are two rankings?
scores.) The brute-force algorithm loops over all
pairs and counts the bad ones, costing exactly comparisons. We can do far better.
Idea 0: divide and conquer, just like mergesort. Split into a left half and a right half . Every inversion is one of three kinds:
- both endpoints in , counted by recursing on ;
- both endpoints in , counted by recursing on ;
- one endpoint in each: a cross inversion, on the left and on the right with .
Counting cross inversions with a double loop costs for the combine step, giving , which the master theorem resolves to , no gain. The combine step is the bottleneck.
Idea 1: count cross inversions during a merge. Suppose the two halves arrive already sorted. Walk them with two cursors exactly as does. When we are about to emit and , the element is smaller than and than everything after it in , so forms an inversion with all remaining elements of at once. Add that count, emit , and move on.
This batching is why the count collapses to linear time: a single comparison reveals inversions, not one. Because is sorted, every element from onward exceeds , so each is inverted with it.
- 1
- 2
- 3
- 4while and do
- 5if then
- 6no inversion
- 7else
- 8inverts with
- 9
- 10return
This runs in , the linear merge pattern. But it demands sorted halves, so we must sort them first: sorting and costs an extra per level, and there are levels, giving . Better than quadratic, but the repeated sorting is wasteful.
Idea 2: sort and count in one pass. We are doing almost all of mergesort's work anyway, so let the recursion return both the inversion count and a sorted copy of its slice. Then the cross-counting merge also produces the sorted output the parent needs, for free.
- 1if then
- 2returnsingle element: no inversions
- 3
- 4left inversions + sort left
- 5right inversions + sort right
- 6cross + merge
- 7return
The helper is just with the counting rule from folded in: whenever it takes from the right half because , it adds the number of elements still waiting in the left half. The combine step is now plain linear, so
This is the same recurrence as mergesort, and the same recursion tree explains it: levels, work each. Counting how disordered a list is costs no more, asymptotically, than sorting it.
A worked count
Run the algorithm on , whose inversions are , , and — three in all. The split gives and . Both halves are already sorted, so the recursive calls return and , and everything rides on the counting merge:
| step | fronts | action | count added | running total |
|---|---|---|---|---|
| vs | emit from | (both of exceed ) | ||
| vs | emit from | |||
| vs | emit from | (only remains in ) | ||
| vs | emit from | |||
| empty | emit from |
Total: , matching the hand count, and the array leaves the merge sorted as , ready for use by the parent call. Step is the batching in action: one comparison charged two inversions, because sortedness of guarantees every element from its cursor onward exceeds the emitted value.
Beyond sorting: faster multiplication
Sorting is not the only home for divide and conquer. The same paradigm beats the grade-school algorithm for multiplying large integers (Karatsuba, three half-size products instead of four, ) and the cubic schoolbook algorithm for multiplying matrices (Strassen, seven block products instead of eight, ). Both spend cheap additions to buy back an expensive multiplication, and both fall straight out of the master theorem below. We give them a lesson of their own: Fast Multiplication.
The master theorem
Every recurrence in this lesson has the form . The recursion-tree analysis we did by hand each time generalizes to a single rule. Compare the branching exponent , the rate at which leaves proliferate, against the work exponent :
The three cases correspond to the three shapes of recursion tree: when the rows grow toward the leaves (Karatsuba), when they are equal every row costs the same (mergesort), and when the root's work dominates. Reading off our examples:
| Algorithm | Recurrence | vs | ||||
|---|---|---|---|---|---|---|
| Mergesort | , balanced | |||||
| Counting inversions | , balanced | |||||
| Inversions, naive combine | , root-heavy | |||||
| Karatsuba | , leaf-heavy |
One last sanity check: it makes no difference whether the combine cost is written or bounded above by . The recurrences and have the same solution. The master theorem depends only on , , and the exponent .
The sort real programs call
Mergesort's clean structure and stability make it the base for the sort that most real programs actually call.
Timsort: exploit the runs already there. The default sort in Python's list
and Java's Arrays.sort for objects is Timsort (Tim Peters, 2002), an
adaptive, stable mergesort. Real data is rarely random: it arrives with long
stretches already ascending or descending — a log file appended over time, a list
re-sorted after a few edits. Timsort scans for these natural runs first,
reversing descending ones in place, and only merges the runs it finds, so an
already-sorted array costs a single pass instead of .
It extends short runs with an insertion sort up to a minimum length, and it merges
runs under a stack invariant that keeps run lengths balanced (the invariant had a
famous bug, found in 2015 by researchers formally verifying the merge policy, that
could overflow the merge stack — since fixed). The through-line is the bottom-up
idea from earlier, made adaptive: instead of blindly doubling from width , start
from the runs already present in the input.
Merging in parallel. The recursion tree's independent subproblems make
mergesort a natural fit for multiple cores: the two recursive sorts run on separate
threads, and the join waits for both. But a naive parallel mergesort is bottlenecked
by its sequential merge at the root. The fix is a parallel merge:
to merge two sorted halves, binary-search the median of one into the other to split
both into balanced pieces that merge independently, recursively. This drops the
span (critical-path length) to while keeping the work
, the design behind the parallel sorts in libraries like Intel TBB
and the C++17 parallel std::sort. Mergesort's sequential, predictable access
pattern — the same property that suits linked lists and disk — also lets it
carve cleanly across cores.3
Takeaways
- Divide and conquer = divide into smaller copies, conquer recursively, combine. Trust the recursion; focus on the split and the merge. The cost is always a recurrence .
- divides at the midpoint and combines with a linear-time whose correctness is a clean loop-invariant argument.
- The recurrence unfolds into a recursion tree with levels of work each, giving .
- Mergesort is stable and worst-case optimal among comparison sorts, at the cost of extra space, ideal for linked lists and external sorting.
- At small sizes the recursion's overhead loses to plain iteration: real implementations cut off to insertion sort below a threshold () or run bottom-up, merging width- runs with no recursion at all.
- Counting inversions reuses the merge: fold a cross-inversion count into so each step adds , sorting and counting together in instead of the brute-force .
- The same machinery beats grade-school arithmetic — see Fast Multiplication for Karatsuba () and Strassen ().
- The master theorem turns the tree into a rule: compare to for leaf-heavy, balanced, or root-heavy behavior.4
Footnotes
- Erickson, Algorithms, Ch. 1 — Recursion: the
recursion fairy
stance of assuming recursive calls already work and focusing on divide and combine. ↩ - CLRS, Ch. 2 (§2.3) — Designing algorithms: mergesort as the canonical divide-and-conquer sort built on a linear-time merge. ↩
- Skiena, The Algorithm Design Manual, §4 — Sorting and Searching: mergesort's stability and suitability for linked lists and external sorting. ↩ ↩2
- CLRS, Ch. 4 — Divide-and-Conquer: the master theorem comparing against the work exponent to classify leaf-heavy, balanced, and root-heavy recurrences. ↩
╌╌ END ╌╌