Recursion Patterns
Print 1 to n, compute a factorial, find a GCD, raise to a power — the same recursive skeleton solves them all.
The problem
Four unrelated-looking tasks land on your desk in one afternoon: print the numbers 1 to n, compute n!, find the greatest common divisor of two numbers, and raise a base to a power fast. You start writing four different loops, each with its own bookkeeping.
Halfway through the third loop you get a nagging feeling that you are typing the same shape over and over. And you are. Underneath, all four are the same recursive move dressed in different clothes. Learn the move once and you stop reinventing it.
A first attempt
Solve each one ad hoc and you get four loops that share nothing:
def power_slow(base, exp):
result = 1
for _ in range(exp): # multiply exp times
result *= base
return resultThat power loop is O(exp) — for 2^1000 it does a thousand multiplications. Worse, you have now written four separate control structures, each with a counter to initialize, advance, and test. There is no shared vocabulary, so every new "compute over the integers" task starts from scratch. The repetition is a hint that a single pattern is hiding.
The insight
Almost every one of these is a linear recursion: reduce the input by one step, recurse, and combine. And two of them — power and GCD — do something even better: they shrink the input by half or toward zero fast, which turns linear work into logarithmic work.
Three reusable skeletons cover the set:
- Do-then-recurse (or recurse-then-do): print
nand recurse, or recurse and then print. Order controls ascending vs descending. - Reduce-and-combine:
factorial(n) = n * factorial(n - 1). - Halve-the-problem: fast exponentiation squares a half-sized result; Euclid's GCD replaces
(a, b)with(b, a mod b).
Once you see them as skeletons, you fill in the blanks instead of reinventing the loop.
How it works
Take fast exponentiation, the most rewarding of the four.
Base case first
base^0 = 1. That anchors the recursion and stops it from running past zero.
Halve the exponent
Compute half = power(base, exp / 2) once. This is the key move — you solve a problem of half the size, not one smaller.
Square and adjust
If exp is even, the answer is half * half. If it is odd, you dropped a factor when you integer-divided, so multiply it back: half * half * base.
power(2, 10)
= power(2, 5)^2
= (2 * power(2, 2)^2)^2
= (2 * (power(2, 1))^2 ... ) -> log2(10) ≈ 4 levels, not 10See why GCD is the same idea
Euclid's algorithm keeps replacing the larger number with the remainder: gcd(a, b) = gcd(b, a mod b), stopping when b hits 0. The numbers shrink fast, so it also finishes in logarithmic steps.
The code
def power(base, exp): # O(log exp)
if exp == 0:
return 1
half = power(base, exp // 2)
if exp % 2 == 0:
return half * half
return half * half * base
def gcd(a, b): # Euclid, O(log min(a, b))
if b == 0:
return a
return gcd(b, a % b)
print(power(2, 10)) # 1024
print(gcd(48, 36)) # 12function power(base: number, exp: number): number {
if (exp === 0) return 1;
const half = power(base, Math.floor(exp / 2));
if (exp % 2 === 0) return half * half;
return half * half * base;
}
function gcd(a: number, b: number): number {
if (b === 0) return a;
return gcd(b, a % b);
}
console.log(power(2, 10)); // 1024
console.log(gcd(48, 36)); // 12static long power(long base, int exp) { // O(log exp)
if (exp == 0) return 1;
long half = power(base, exp / 2);
if (exp % 2 == 0) return half * half;
return half * half * base;
}
static int gcd(int a, int b) { // O(log min(a, b))
if (b == 0) return a;
return gcd(b, a % b);
}
// power(2, 10) -> 1024, gcd(48, 36) -> 12long power(long base, int exp) { /* O(log exp) */
if (exp == 0) return 1;
long half = power(base, exp / 2);
if (exp % 2 == 0) return half * half;
return half * half * base;
}
int gcd(int a, int b) { /* O(log min(a, b)) */
if (b == 0) return a;
return gcd(b, a % b);
}
/* power(2, 10) -> 1024, gcd(48, 36) -> 12 */long long power(long long base, int exp) { // O(log exp)
if (exp == 0) return 1;
long long half = power(base, exp / 2);
if (exp % 2 == 0) return half * half;
return half * half * base;
}
int gcd(int a, int b) { // O(log min(a, b))
if (b == 0) return a;
return gcd(b, a % b);
}
// power(2, 10) -> 1024, gcd(48, 36) -> 12Complexity
| Pattern | Example | Time | Space |
|---|---|---|---|
| Do-then-recurse | print 1..n | O(n) | O(n) |
| Reduce-and-combine | factorial | O(n) | O(n) |
| Halve-the-problem | fast power | O(log exp) | O(log exp) |
| Euclid's GCD | gcd(a, b) | O(log min(a,b)) | O(log min(a,b)) |
The lesson of the table: shrinking the input by a step costs linear time, but shrinking it by half costs logarithmic time. Same skeleton, wildly different speed.
When to use it
Match the skeleton to the shrink
If a problem gets one element smaller per call, reach for the reduce-and-combine skeleton — clean, but linear. If you can cut the problem in half each call (exponentiation, GCD, binary search), the halving skeleton buys you a logarithmic speedup practically for free. Recognizing which shrink applies is more valuable than memorizing any single function.
Practice
Recap
- A handful of recursive skeletons — do-then-recurse, reduce-and-combine, halve-the-problem — cover most integer recursion.
- Order of work versus recursion controls direction; the rate of shrink controls complexity.
- Halving the input turns linear work into logarithmic work.
How is this guide?
Last updated on