Mustaque Nadim Academy
Recursion & Math

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 result

That 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 n and 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 10

See 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))   # 12
function 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)); // 12
static 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) -> 12
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;
}

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) -> 12

Complexity

PatternExampleTimeSpace
Do-then-recurseprint 1..nO(n)O(n)
Reduce-and-combinefactorialO(n)O(n)
Halve-the-problemfast powerO(log exp)O(log exp)
Euclid's GCDgcd(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

On this page