Algorithmic Patterns
Divide and conquer
Split the problem, solve the halves, merge
Here is one of the most productive ideas in all of computing, and it fits in three words: divide, conquer, combine. Cut the input into smaller independent parts, solve each part by the very same method (that is the recursion), then merge the partial answers into the whole. The pattern is easy. The interesting question is always the bill - and the bill is written as a recurrence like , which the Master Theorem reads off almost by eye.
The template, in three moves
1 - Divide. Split the size- problem into subproblems, each of size . For merge sort, halves of size . 2 - Conquer. Solve each subproblem by recursing - until you hit a base case so small the answer is obvious (a one-element list is already sorted). 3 - Combine. Glue the sub-answers together in extra work. In merge sort, combining is the merge: zip two sorted halves into one, in . That single line - how much the divide and combine cost per call - is what decides the whole running time.
Counting the cost: recurrences and the Master Theorem
A recursive algorithm's running time is defined in terms of itself on smaller inputs. If a problem of size makes recursive calls on inputs of size , plus non-recursive work to split and combine, then its total time obeys
Merge sort makes calls on halves () and merges in linear time, so . To turn that self-referential equation into a plain Big-O, picture the recursion tree: level has nodes, each of size , so the work on that level is . The total is the sum down all levels - and everything hinges on a single race between two numbers: (how fast the tree widens) versus (how fast the work shrinks, when ).
The Master Theorem - three outcomes of one comparison
For with , compare the tree's growth exponent with :
The intuition is a geometric series. If each level does the work of the one above it, then either the bottom level dominates (ratio , Case 1), every level is equal so you multiply by the depth (ratio , Case 2), or the top level dominates (ratio , Case 3). Merge sort is the balanced case: , so .
| Algorithm | Recurrence | Case | Result |
|---|---|---|---|
| Binary search | 2 | O(log n) | |
| Merge sort | 2 | O(n log n) | |
| Max-subarray (D&C) | 2 | O(n log n) | |
| Karatsuba multiply | 1 | O(n^1.585) |
See it work
Two coupled toys in one. Top: watch merge sort build its recursion tree - step down as the 8-element array divides into halves until every leaf is a single element, then step back up as each pair of sorted runs merges (the amber pair is the two fronts being compared; the smaller one drops into the parent). Bottom: tune , , and the exponent of and read off which Master Theorem case you land in - the presets drop you onto the real algorithms above.
Start: the whole 8-element array is one unsorted problem - the root of the tree.
recurrence: T(n) = 2 T(n/2) + n
compare: a = 2 vs b^d = 2 · log2a = 1
Case 2a = b^d - balanced
T(n) = O(n log n)
Work per recursion level (each step x a/bd)
Case 2: every level does equal work.
The same shape, everywhere
Binary search is divide and conquer with a trivial combine: it throws away a whole half after one comparison and never has to glue anything back, so (Case 2 with ). Karatsuba multiplies two -digit numbers with only half-size multiplications instead of the schoolbook 4 - dropping one subproblem changes into , so becomes . And the maximum-subarray problem splits the array in two, recurses on each half, and combines by finding the best sub-array that crosses the midpoint in - the same as merge sort. Here is the canonical combine, the merge:
def merge_sort(a):
if len(a) <= 1: # base case: 0 or 1 items are already sorted
return a
mid = len(a) // 2 # DIVIDE at the midpoint
left = merge_sort(a[:mid]) # CONQUER - recurse on each half
right = merge_sort(a[mid:])
return merge(left, right) # COMBINE the two sorted halves
def merge(left, right): # zip two sorted runs into one, in O(n)
out, i, j = [], 0, 0
while i < len(left) and j < len(right):
if left[i] <= right[j]: # take the smaller of the two fronts
out.append(left[i]); i += 1
else:
out.append(right[j]); j += 1
out.extend(left[i:]) # one side is empty - drain the other
out.extend(right[j:])
return outThe subproblems must be independent
Divide and conquer only pays off when the pieces don't need to talk to each other while being solved - sorting the left half never depends on the right. When subproblems overlap (the same smaller problem recurs across branches, as in naive Fibonacci), plain recursion re-does exponential work; that is the world of dynamic programming, where you memoize instead. D&C is the independent-subproblem cousin of DP.
Check yourself
In the Master Theorem, when the work is the same at every level of the recursion tree (Case 2), how do you get the total?
Binary search does one comparison, then recurses on half: T(n)=T(n/2)+O(1). What does it solve to?
State the master recurrence, and the three outcomes of comparing a with b^d (where f(n)=O(n^d)).
Recall the three cases. Try to state it, then check.
Lock it in
- Divide into independent subproblems, conquer by recursing to a base case, then combine - and the per-call divide-plus-combine cost is what decides the total running time.
- The bill is a recurrence ; the Master Theorem reads off the answer by racing the growth exponent against (where ).
- Three outcomes: leaves win, balanced (an extra factor), or root wins. Merge sort is the balanced case, .
- The subproblems must be independent; when they overlap (naive Fibonacci) you memoize instead - that is dynamic programming.
Primary source
For divide and conquer and the Master Theorem derived slowly by hand - recursion trees, the three cases, worked recurrences - study Abdul Bari - Algorithms. For the rigorous treatment (proofs of the theorem, Karatsuba, and the max-subarray algorithm), see MIT 6.006 - Introduction to Algorithms (Spring 2020).
Ask your teacher
Want the Master Theorem's third condition (the regularity check that Case 3 quietly needs), or the Akra-Bazzi method for uneven splits like ? Ask and I'll build the follow-up. Worth planting now: this is the exact shape behind merge sort and binary search. The Master Theorem is why halving a problem costs - and it is the same innocent that makes an attention layer's cost sting so much: doubling the context length quadruples the work, with no halving to rescue you.