Functions that call themselves. Base case and recursive case.
Self-serve tutorial - low prerequisites, straightforward concepts.
Recursion is the programming version of a “self-similar” story: to solve a big problem, you describe how to solve a smaller version of the same problem—until you hit a stopping point.
A recursive function solves a problem by calling itself on smaller inputs. It must have (1) a base case that stops, and (2) a progress measure that strictly decreases each call so it can’t recurse forever. The call stack remembers “where you were” so results can return back up.
Some problems are naturally self-similar: the overall structure repeats inside smaller parts.
Recursion is a technique for expressing that self-similarity directly in code: a function calls itself to solve a smaller version of the same problem.
A function is recursive if it (directly or indirectly) invokes itself.
A well-formed recursive function has two essential parts:
1) Base case: a condition where the function returns an answer without making another recursive call.
2) Recursive case: a rule that reduces the problem to one or more smaller instances of itself.
To prevent infinite recursion, each recursive call must make progress toward the base case.
Think of a quantity that always decreases and can’t decrease forever. Common measures:
n decreases: n → n − 1 until n = 0hi − lo shrinks each calllen(xs) goes downMathematically, you want a well-founded measure: there is no infinite strictly-decreasing sequence. The natural numbers ℕ with “>” are well-founded, because you can’t keep subtracting 1 forever without hitting 0.
Factorial is often defined recursively:
In code-like pseudocode:
fact(n):
if n == 0: return 1
else: return n * fact(n-1)Here:
n == 0n decreases by 1 each callYou already know stacks (LIFO). Recursion uses the call stack automatically.
When fact(4) calls fact(3), the program needs to remember:
n was (4)That “remembering” is stored in a stack frame on the call stack. Then fact(3) calls fact(2), pushing another frame, and so on.
When the base case returns, results flow back upward, popping frames as each call completes.
Many recursive functions can be rewritten as loops. Recursion is often clearer when:
But recursion can use more stack space and can overflow if too deep. You’ll learn to choose based on clarity and constraints.
People new to recursion often focus on “the function calls itself” and forget that recursion is really about two responsibilities:
If either part is wrong, you get infinite recursion, wrong answers, or runtime errors.
When reasoning about recursion, use this disciplined viewpoint:
1) Assume the recursive call works on smaller inputs.
2) Use it to build the solution for the current input.
3) Verify that you eventually reach the base case.
This is basically informal induction.
Define S(n) = 1 + 2 + … + n.
Recursive definition:
Pseudocode:
sumTo(n):
if n == 0: return 0
else: return n + sumTo(n-1)Call sumTo(3):
sumTo(3) needs 3 + sumTo(2)sumTo(2) needs 2 + sumTo(1)sumTo(1) needs 1 + sumTo(0)sumTo(0) returns 0Now unwind:
sumTo(1) returns 1 + 0 = 1sumTo(2) returns 2 + 1 = 3sumTo(3) returns 3 + 3 = 6Notice the call stack is LIFO:
Base cases depend on the problem. Two patterns show up often:
1) Minimal input base case
n == 0[]2) Small threshold base case
len(arr) ≤ 1lo > hiThe key is that the base case is guaranteed to occur if the recursive case keeps making progress.
| Shape | Calls per frame | Typical data | Example | Key measure |
|---|---|---|---|---|
| Linear recursion | 1 | integers, lists | factorial, list length | n, len |
| Divide-and-conquer | 2+ | arrays, problems split | merge sort | problem size |
| Tree recursion | varies | trees/graphs | DFS on tree | height/remaining nodes |
At difficulty 2, linear recursion is the easiest place to get fluent, but you should recognize the others so recursion doesn’t feel mysterious later.
A calls B calls A:
A(x):
... return B(x-1)
B(x):
... return A(x-1)This is still recursion (mutual recursion). It still needs:
Each recursive call adds a stack frame. If the recursion is too deep, you can hit a stack overflow.
For linear recursion on large n, a loop is often safer. Later, you’ll learn techniques like tail recursion (and language-specific optimizations), but the first step is simply being aware that recursion consumes stack space proportional to depth.
Many recursion bugs have a base case, but still never reach it because the recursive step doesn’t move closer.
Example of a broken recursive case:
countDown(n):
if n == 0: return
else: countDown(n) # progress measure does not decreaseIt has a base case (n == 0) but never reaches it.
So we need a way to prove we’re moving toward termination.
A progress measure is a value m(input) ∈ ℕ (often) such that:
Formally, if f calls f on inputs x → x′, we want:
m(x′) < m(x)
Since ℕ has no infinite strictly decreasing sequences, recursion must eventually stop.
1) Integer countdown
nn − 12) List processing
xstail(xs) (length decreases by 1)3) Range shrinking
(lo, hi)Recursion correctness typically follows the same structure:
1) Base case correctness: show the function returns the right answer on the base case.
2) Inductive step: assume the function works for all smaller measures; prove it works for the current input.
You don’t need formal proofs for every function, but this mindset prevents “wishful recursion.”
Suppose we define a function sum(xs).
Recursive idea:
Let xs = [x₀, x₁, …, xₖ].
Progress measure: m(xs) = len(xs). Each call uses rest where len(rest) = len(xs) − 1.
Correctness intuition:
Broken variant:
fact(n):
if n == 0: return 1
else: return n * fact(n+1)This moves away from the base case: n grows, and you never hit 0.
The progress measure test catches this immediately:
Sometimes the input is a structure (like a tree). You can still measure progress:
Even if you never compute the measure explicitly, you should be able to point to it in your reasoning.
Before you run your code, answer:
1) What is the base case?
2) What input(s) does the recursive call use?
3) What is the progress measure?
4) Does the measure strictly decrease on every path?
5) Does the recursive case combine results correctly?
If you can answer these, the recursion is usually correct and terminating.
Recursion is not just a coding trick; it’s a way to express algorithmic structure.
Many major algorithm families are easiest to understand recursively first, and then (sometimes) optimized into iterative forms.
Divide-and-conquer means:
1) Split the problem into smaller subproblems
2) Solve each subproblem (often recursively)
3) Combine the results
Example: merge sort
Progress measure: n = array length; each call handles roughly n/2.
This is the gateway to analyzing runtime with recurrence relations like:
T(n) = 2T(n/2) + O(n)
You’re not solving recurrences yet in this node, but recursion is what creates them.
Many problems have:
A common path:
1) write a clean recursive solution
2) notice repeated calls
3) add memoization (cache) → dynamic programming
Classic example: Fibonacci.
Naive recursion:
This is correct but repeats work. Memoization keeps the recursive clarity while improving performance.
Depth-first search (DFS) on a tree is naturally recursive:
Progress measure: remaining nodes / subtree height.
(For general graphs, you must also track visited nodes to avoid cycles—otherwise recursion may not terminate even if depth decreases in some branches.)
Anything nested—parentheses, JSON-like structures, expression trees—often becomes simpler with recursion.
Since you already know stacks, here’s the key connection:
This is a powerful translation:
| You have… | Recursive solution uses… | Iterative rewrite uses… |
|---|---|---|
| Nested structure | call stack frames | explicit stack data structure |
| Need to return after subcall | return address stored in frame | store state in stack entries |
Understanding this makes recursion feel less like magic and more like a design choice.
Recursion is great when it matches the problem structure. But you should consider alternatives when:
Even then, learning recursion first is often the best route to a correct solution.
Compute 4! using a recursive definition and show every intermediate step.
Definition:
Start with the goal:
4! = 4 · 3!
Expand 3! using the same rule:
3! = 3 · 2!
So:
4! = 4 · (3 · 2!)
Expand 2!:
2! = 2 · 1!
So:
4! = 4 · (3 · (2 · 1!))
Expand 1!:
1! = 1 · 0!
So:
4! = 4 · (3 · (2 · (1 · 0!)))
Use the base case:
0! = 1
So:
4! = 4 · (3 · (2 · (1 · 1)))
Now multiply while unwinding (returning):
1 · 1 = 1
2 · 1 = 2
3 · 2 = 6
4 · 6 = 24
Final answer:
4! = 24
Insight: The recursion “goes down” building a chain of deferred multiplications, and the base case starts the “unwind” where results flow back up. The call stack stores those deferred operations.
Let xs = [5, 2, 7]. Define recursively:
Compute sum([5, 2, 7]) with explicit substitutions.
Start:
sum([5, 2, 7]) = 5 + sum([2, 7])
Apply the rule again:
sum([2, 7]) = 2 + sum([7])
So:
sum([5, 2, 7]) = 5 + (2 + sum([7]))
Apply the rule to the last non-empty list:
sum([7]) = 7 + sum([])
So:
sum([5, 2, 7]) = 5 + (2 + (7 + sum([])))
Use the base case:
sum([]) = 0
So:
sum([5, 2, 7]) = 5 + (2 + (7 + 0))
Compute:
7 + 0 = 7
2 + 7 = 9
5 + 9 = 14
Final answer:
sum([5, 2, 7]) = 14
Insight: The progress measure is len(xs). Each call removes one element, guaranteeing termination, and the combination step (x + …) reconstructs the full sum.
Binary search checks the middle of a sorted range and recurses into a smaller half.
Suppose arr is sorted and we search for target in indices lo..hi.
Progress measure: m(lo, hi) = max(0, hi − lo + 1).
Base case idea:
If lo > hi, the range is empty, so return “not found.”
This matches m(lo, hi) = 0.
Recursive case idea:
Let mid = ⌊(lo + hi)/2⌋.
Compare arr[mid] with target.
If target < arr[mid], recurse on left half:
(lo, hi) → (lo, mid − 1)
New measure:
m' = (mid − 1) − lo + 1 = mid − lo
Old measure:
m = hi − lo + 1
Since mid ≤ hi, we get:
mid − lo < hi − lo + 1
So m' < m.
If target > arr[mid], recurse on right half:
(lo, hi) → (mid + 1, hi)
New measure:
m' = hi − (mid + 1) + 1 = hi − mid
Since mid ≥ lo, we have:
hi − mid < hi − lo + 1
So m' < m.
Therefore, on every recursive path, the measure strictly decreases until the base case lo > hi occurs.
Insight: You don’t have to ‘feel’ that recursion terminates—you can measure it. Range size is a clean, reliable progress measure for divide-and-conquer search.
Recursion = solving a problem by calling the same function on smaller instances of the problem.
Every recursive function needs a base case (stop) and a recursive case (reduce).
A progress measure m(input) ensures termination by strictly decreasing on each recursive call (often into ℕ).
The call stack acts like an implicit stack that stores “what to do after the recursive call returns.”
Reasoning about recursion works well with an induction-like mindset: assume smaller cases work, then build the current case.
Many major algorithm families (divide-and-conquer, dynamic programming, tree traversals) are naturally expressed recursively.
Recursion can be rewritten iteratively by managing your own explicit stack, which can help avoid deep call stacks.
Missing or incorrect base case (e.g., forgetting n == 0, or using the wrong stopping condition).
No progress toward the base case (e.g., calling f(n) → f(n) or moving in the wrong direction f(n) → f(n+1)).
Off-by-one errors in the reduced input (e.g., binary search recursing on (lo, mid) instead of (lo, mid − 1), causing non-decreasing ranges).
Assuming recursion is always slower or always better: the right choice depends on depth, clarity, and constraints.
Write a recursive definition for power(n) = 2ⁿ for n ∈ ℕ, including a base case. Then compute 2⁵ by expanding your recursion.
Hint: Use 2⁰ = 1 as the base case, and reduce n by 1 each step.
Recursive definition:
Expansion:
pow2(5)
= 2 · pow2(4)
= 2 · (2 · pow2(3))
= 2 · (2 · (2 · pow2(2)))
= 2 · (2 · (2 · (2 · pow2(1))))
= 2 · (2 · (2 · (2 · (2 · pow2(0)))))
= 2 · (2 · (2 · (2 · (2 · 1))))
= 32
Consider this function:
sumDown(n):
if n == 0: return 0
else: return n + sumDown(n)
Identify the bug using a progress measure, and fix the function.
Hint: What is the intended progress measure? Does it decrease in the recursive call?
Intended progress measure is m(n) = n. But the recursive call uses sumDown(n), so m does not decrease (it stays equal), meaning the base case is never reached for n > 0.
Fix:
sumDown(n):
if n == 0: return 0
else: return n + sumDown(n − 1)
Now m(n) decreases by 1 each call until n = 0.
Define a recursive function len(xs) that returns the length of a list xs, using:
Then evaluate len([9, 8, 7, 6]) step by step.
Hint: Your base case should be the empty list. Your recursive call should use rest (the tail).
Definition:
Evaluation:
len([9, 8, 7, 6])
= 1 + len([8, 7, 6])
= 1 + (1 + len([7, 6]))
= 1 + (1 + (1 + len([6])))
= 1 + (1 + (1 + (1 + len([]))))
= 1 + (1 + (1 + (1 + 0)))
= 4