Recursion Basics
One-liner: recursion solves a problem by solving a slightly smaller copy of the same problem, and stops at a case so small the answer is obvious.
The analogy
Russian nesting dolls. To find out how many dolls are inside, you open the biggest one and ask the same question of the doll inside it. That doll opens the next, and so on, until you reach the smallest doll, which has nothing inside (the base case). Then the answers travel back out: "0 inside", so this one holds 1, so the next holds 2, and so on. Every doll does the same small job and trusts the doll inside to answer for itself.
Recognition signals
- The problem contains a smaller version of itself: a tree is a root plus smaller trees, a list is a first item plus a shorter list.
- You need to try every choice and each choice leaves a smaller problem (Backtracking).
- The answer is defined in terms of earlier answers (
fib(n) = fib(n-1) + fib(n-2)), which later becomes dynamic programming. - The data is nested or branching, such as trees and folders, where a loop would need its own stack.
Step-by-step walkthrough
Factorial(3) means 3 × 2 × 1. Follow the calls down, then the answers back up:
Factorial(3)is not a base case. It callsFactorial(2)and waits.Factorial(2)callsFactorial(1)and waits.Factorial(1)is the base case: it returns1straight away.- Back in
Factorial(2):2 × 1 = 2, return2. - Back in
Factorial(3):3 × 2 = 6, return6.
While Factorial(3) waits, the computer remembers it on the call stack, a pile with one entry per unfinished call. Calls pile up on the way down and are removed on the way back.
Code template
// Factorial: n! = n × (n-1)! — a big problem defined by a slightly smaller copy of itself.
func Factorial(n int) int {
if n <= 1 { // 1. base case: the smallest problem, answered directly. Without it the calls never stop.
return 1
}
return n * Factorial(n-1) // 2. trust the smaller call, 3. combine its answer with this level's work
}if n <= 1 { return 1 }The base case is what stops the recursion. Without it the function calls itself forever, the stack fills up, and Go crashes with a stack overflow.
Factorial(n-1)The call must move toward the base case. n-1 shrinks; calling Factorial(n) again would never end.
n * Factorial(n-1)Trust that the smaller call returns the right answer, then add only this level's own work. Do not try to trace the whole chain in your head.
See the call stack
Trace logs when each call starts and ends. Notice the order: every enter comes before any exit, and the exits come back in the reverse order.
// Trace records when each call starts and finishes, so you can see the call stack grow and unwind.
// Calls "go down" until the base case, then answers come back up in the opposite order.
func Trace(n int) []string {
var log []string
var walk func(int)
walk = func(k int) {
log = append(log, fmt.Sprintf("enter %d", k))
if k > 0 {
walk(k - 1)
}
log = append(log, fmt.Sprintf("exit %d", k))
}
walk(n)
return log
}For Trace(2) the log is enter 2, enter 1, enter 0, exit 0, exit 1, exit 2. The last call to start is the first to finish, which is exactly how a stack of plates behaves (Stack Basics).
The real solutions
Sum of a list — the sum is the first number plus the sum of the rest:
// SumList adds the numbers in a slice: the sum is the first number plus the sum of everything after it.
func SumList(nums []int) int {
if len(nums) == 0 {
return 0 // nothing left to add
}
return nums[0] + SumList(nums[1:])
}Reverse a string — the reverse of the rest, then the first letter at the end:
// Reverse flips a string: the reverse is the reverse of the rest, followed by the first letter.
func Reverse(s string) string {
if len(s) <= 1 {
return s
}
return Reverse(s[1:]) + s[:1]
}Power by halving — the same idea as Pow(x, n). Compute the half once and reuse it:
// Power computes x^n by halving the exponent each call, so 2^1000 needs about 10 calls, not 1000.
func Power(x float64, n int) float64 {
if n == 0 {
return 1
}
half := Power(x, n/2) // computed ONCE and reused: calling Power twice here would undo the speed-up
if n%2 == 0 {
return half * half
}
return half * half * x
}When recursion repeats work — FibSlow recomputes the same values again and again (21,891 calls for n = 20). FibMemo stores each answer the first time, so there are only about 2n calls. That one change is the whole idea of dynamic programming:
// FibSlow shows why recursion can explode: fib(n) calls fib(n-1) and fib(n-2), and those repeat the same work.
// calls counts every invocation so you can see the growth.
func FibSlow(n int, calls *int) int {
*calls++
if n < 2 {
return n
}
return FibSlow(n-1, calls) + FibSlow(n-2, calls)
}
// FibMemo remembers answers it has already computed, so each n is solved once: the idea behind dynamic programming.
func FibMemo(n int, memo map[int]int) int {
if n < 2 {
return n
}
if v, ok := memo[n]; ok {
return v
}
memo[n] = FibMemo(n-1, memo) + FibMemo(n-2, memo)
return memo[n]
}Complexity
| Approach | Time | Space |
|---|---|---|
| Factorial, SumList, Reverse (one call each level) | O(n) | O(n) call stack |
| Power by halving | O(log n) | O(log n) call stack |
| FibSlow (two calls each level) | O(2ⁿ) | O(n) call stack |
| FibMemo | O(n) | O(n) memo + stack |
Recursion is never free in memory: each unfinished call takes a slot on the stack, so the depth of the recursion is part of the space cost.
Common mistakes
Practice ladder
Start with the first two in this page's code, then solve these. For each, say the base case out loud before writing anything.
- 1.Climbing StairsEasyHow many ways to reach step n, given the ways to reach the two steps before it?
- 2.Reverse Linked ListEasyReverse the rest of the list first, then fix one pointer.
- 3.Maximum Depth of Binary TreeEasyWhat is the depth of an empty tree?
- 4.Invert Binary TreeEasyAssume both subtrees are already inverted.
- 5.Pow(x, n)MediumHalve the exponent and reuse the result.
- 6.Generate ParenthesesMediumEach call adds one character; when is each character allowed?
When these feel natural, continue with Trees, where almost every solution is a three-line recursive function.
Which pattern? Drills
Count how many files are inside a folder, including every file in its sub-folders and their sub-folders, to any depth.
Which pattern?