Recursion Field Notes
A function that calls itself. One branch (factorial) and two branches (fibonacci). After this page you can trace any recursive call by hand, draw its call stack or call tree, say its Big-O, and turn it into a loop when the loop is simpler.
0Why recursion exists
A gift box. You open it and find a smaller box inside. You open that one and find a smaller box again. You keep opening until you reach the smallest box, which holds the gift. Then you close the boxes back up from the inside out. The smallest box is the base case. Every larger box "contains a smaller version of the same problem": that is the recursive case.
Recursion is when a function calls itself, usually with a different (smaller) input. A function that does this is a recursive function. It breaks a problem into smaller sub-problems of the same shape, and then solves them in reverse order: the smallest one is finished first, and the biggest one is finished last.
Every recursive function has exactly two parts:
- Base case: the input is so small that you answer directly, with no further call. Without it the function never stops.
- Recursive case: the function calls itself on a smaller input and uses that result to build its own answer.
There are two shapes. One-branch recursion makes one recursive call per step (factorial, lesson 8). Multi-branch recursion makes two or more (fibonacci, lesson 9), which draws a tree of calls. It is usually possible to convert recursion to iteration and back. Sometimes the loop is simpler, sometimes the recursion is. Trees and graphs, which come later in the course, are where recursion is the natural choice.
1One branch: factorial
n! as a smaller copy of itself
n factorial, written n!, is the product of all whole numbers from n down to 1: n! = n × (n-1) × (n-2) × ... × 1. Example: 5! = 5 × 4 × 3 × 2 × 1 = 120.
Look at the same product again: 5! = 5 × 4!, and 4! = 4 × 3!, and so on. Each factorial is "n times a smaller factorial". That is the recursive case. The chain has to end somewhere: 1! = 1 and, by definition, 0! = 1. That is the base case.
# Recursive implementation of n! (n-factorial) calculation
def factorial(n):
# Base case: n = 0 or 1
if n <= 1:
return 1
# Recursive case: n! = n * (n - 1)!
return n * factorial(n - 1)
Two things to notice. The base case comes first, so it is checked before any new call. And the recursive call is on a smaller input (n - 1), so every call moves toward the base case.
Trace factorial(5): down, then up
factorial(5) wants to return 5 * factorial(4). It cannot finish until factorial(4) answers. So it waits, and factorial(4) calls factorial(3), which calls factorial(2), which calls factorial(1). That last one is the base case and returns 1 without calling anyone. Now the waiting calls finish in reverse: 2 × 1 = 2 (that is 2!), 3 × 2 = 6, 4 × 6 = 24, 5 × 24 = 120.
| Step | Call | n <= 1? | Does | Waits for | Returns |
|---|---|---|---|---|---|
| ① | factorial(5) | no | 5 * factorial(4) | factorial(4) | (later) 5 × 24 = 120 |
| ② | factorial(4) | no | 4 * factorial(3) | factorial(3) | (later) 4 × 6 = 24 |
| ③ | factorial(3) | no | 3 * factorial(2) | factorial(2) | (later) 3 × 2 = 6 |
| ④ | factorial(2) | no | 2 * factorial(1) | factorial(1) | (later) 2 × 1 = 2 |
| ⑤ | factorial(1) | yes | return 1 | nobody | 1, immediately |
Read the table top to bottom for the calls, then bottom to top for the returns. Step ⑤ returns first; step ① returns last. That is what "solve the sub-problems in reverse order" means.
See it happen: a traced version
Add a depth argument and print with indentation. This is the single best way to build intuition; do it once for every recursive function you write this week.
def factorial_traced(n, depth=0):
pad = " " * depth
print(f"{pad}factorial({n}) called")
if n <= 1:
print(f"{pad}base case -> return 1")
return 1
sub = factorial_traced(n - 1, depth + 1) # wait here until the smaller call returns
print(f"{pad}factorial({n}) = {n} * {sub} = {n * sub}")
return n * sub
factorial_traced(5)
# factorial(5) called
# factorial(4) called
# factorial(3) called
# factorial(2) called
# factorial(1) called
# base case -> return 1
# factorial(2) = 2 * 1 = 2
# factorial(3) = 3 * 2 = 6
# factorial(4) = 4 * 6 = 24
# factorial(5) = 5 * 24 = 120
The line sub = factorial_traced(n - 1, ...) is where the function pauses. Everything after that line runs only when the smaller call has fully finished. Five copies of the function are alive at the same time, each paused at that line with its own n.
If you delete if n <= 1: return 1, factorial(5) calls factorial(4), ... factorial(0), factorial(-1), and so on forever. Each call uses a little memory, so the program crashes with a stack overflow: the call stack ran out of room. In Python the crash is RecursionError: maximum recursion depth exceeded. Section 2 explains the stack.
2The call stack: where the space goes
A pile of sticky notes. Each time a function is called you write its variables on a note and put it on top of the pile. When that function returns, you throw the top note away and continue with the note under it. The pile is the call stack. Its height is the recursion depth.
factorial creates no list, no dictionary, nothing. So why is its space O(n)? Because recursion uses an implicit stack: the function call stack that the language keeps for you. Each call gets a stack frame holding its own n and the place to return to. That is how "return from this call to the previous one" works at all. factorial(5) has five frames alive at once, so the stack holds n frames: O(n) space even though you wrote no data structure.
| factorial(n) | Cost | Why |
|---|---|---|
| Time | O(n) | n calls (n, n-1, ..., 1), each does one comparison and one multiplication: O(1) work per call |
| Space | O(n) | n frames on the call stack at the peak; no explicit data structure, but the implicit stack still counts |
Stack overflow and Python's recursion limit
The call stack has a fixed size. Go too deep and you get a stack overflow. Python guards this with a depth limit, 1000 frames by default, and raises RecursionError when you cross it. You can raise the limit for a deep but correct recursion (a linked list of 5000 nodes, for example). It does not fix a missing base case; it only delays the crash.
import sys
sys.getrecursionlimit() # 1000 by default in CPython
sys.setrecursionlimit(10_000) # allow deeper recursion (use with care)
factorial(3000) # ok now; RecursionError under the default limit
def no_base_case(n):
return n * no_base_case(n - 1) # 5, 4, 3, ..., -900, ... never stops
no_base_case(5) # RecursionError: maximum recursion depth exceeded
If an interviewer asks "what if n is a million?", the honest answer is: recursion would blow the stack, so I would convert to a loop (next heading) or use an explicit stack. Saying sys.setrecursionlimit alone is not a real fix at that size.
Iteration vs recursion
The same product with a loop. NeetCode's version stores the running answer in res and decrements n until it reaches 1:
n = 5
res = 1
while n > 1:
res = res * n
n -= 1
# res is 120, n is 1
# the same thing as a function
def factorial_iter(n):
res = 1
while n > 1:
res = res * n
n -= 1
return res
Recursive factorial | Iterative factorial_iter | |
|---|---|---|
| Time | O(n) | O(n) |
| Space | O(n) call stack | O(1) two variables |
| Where the answer lives | spread across n paused frames | in res |
| Reads best when | the problem is a tree (later lessons) | the problem is a straight line, like this one |
For factorial the loop is simpler and cheaper. That is not always true. For trees, where every node has two children, the recursive version is usually a few lines and the loop needs you to manage your own stack. Rule: iterative is more space-efficient (no call stack); recursive is often shorter and clearer for branching problems.
3Two branches: fibonacci
Multi-branch recursion, where a function calls itself more than once, is more common in practice than one-branch. The classic example is the Fibonacci sequence. It starts 0, 1, and each next number is the sum of the two before it:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
Base cases: F(0) = 0 and F(1) = 1. Recursive case: F(n) = F(n-1) + F(n-2). A formula that defines a value in terms of smaller values of itself is called a recurrence relation: fib(n) = fib(n-1) + fib(n-2). You will meet recurrence relations again in dynamic programming.
# Recursive implementation to calculate the n-th Fibonacci number
def fibonacci(n):
# Base case: n = 0 or 1
if n <= 1:
return n
# Recursive case: fib(n) = fib(n - 1) + fib(n - 2)
return fibonacci(n - 1) + fibonacci(n - 2)
Note the base case returns n itself, not 1: fibonacci(0) is 0 and fibonacci(1) is 1. The last line calls the function twice. That is a branch factor of two: every call spawns two more, so the calls form a tree instead of a chain.
Trace fibonacci(5): a tree, not a chain
fibonacci(5) = fibonacci(4) + fibonacci(3). Python evaluates the left call first, so fibonacci(4) runs to completion (it calls fibonacci(3) + fibonacci(2), and so on down to 1 or 0) before fibonacci(3) on the right even starts. Then the values come back up: fibonacci(4) = 3, fibonacci(3) = 2, so fibonacci(5) = 5.
F(3) is computed twice (dashed regions) and F(2) three times (orange rings), each time from scratch. That repeated work is why plain recursive fibonacci is slow, and it is exactly what memoization and dynamic programming remove (section 4, move 4). Values in orange are what each call returns; left children finish before right children.Trace table: fibonacci(4), every call and every return
The tree hides the order. This table shows the real order of events for fibonacci(4): 9 calls, 9 returns, and the stack after each event (bottom on the left, top on the right).
| # | Event | Call stack after it | Value |
|---|---|---|---|
| 1 | call F(4) | [F4] | needs F(3) + F(2) |
| 2 | call F(3) (left) | [F4, F3] | needs F(2) + F(1) |
| 3 | call F(2) (left) | [F4, F3, F2] | needs F(1) + F(0) |
| 4 | call F(1) (left) | [F4, F3, F2, F1] | base case |
| 5 | return from F(1) | [F4, F3, F2] | 1 |
| 6 | call F(0) (right) | [F4, F3, F2, F0] | base case |
| 7 | return from F(0) | [F4, F3, F2] | 0 |
| 8 | return from F(2) | [F4, F3] | 1 + 0 = 1 |
| 9 | call F(1) (right) | [F4, F3, F1] | base case |
| 10 | return from F(1) | [F4, F3] | 1 |
| 11 | return from F(3) | [F4] | 1 + 1 = 2 |
| 12 | call F(2) (right) | [F4, F2] | needs F(1) + F(0), again |
| 13 | call F(1) (left) | [F4, F2, F1] | base case |
| 14 | return from F(1) | [F4, F2] | 1 |
| 15 | call F(0) (right) | [F4, F2, F0] | base case |
| 16 | return from F(0) | [F4, F2] | 0 |
| 17 | return from F(2) | [F4] | 1 + 0 = 1 |
| 18 | return from F(4) | [] | 2 + 1 = 3 |
Two facts fall out of the table. The stack never holds more than 4 frames (n frames in general), so space is O(n). And F(2) was fully recomputed at step 12 even though step 8 already knew the answer.
Count the calls
A quick experiment before the theory: add a counter.
calls = 0
def fib_counted(n):
global calls
calls += 1
if n <= 1:
return n
return fib_counted(n - 1) + fib_counted(n - 2)
# n: 4 5 10 20
# calls: 9 15 177 21891 roughly doubles every time n grows by 1
Time complexity: count the tree level by level
Look at the tree by levels (rows). Level 0 has 1 node, the root. Each node makes two calls, so level 1 can hold 2 nodes, level 2 can hold 4, level 3 can hold 8. Each level can hold twice the previous one. The deepest level is n (the path n, n-1, n-2, ..., 0 down the left side). So the total number of nodes is at most:
1 + 2 + 4 + 8 + ... + 2^n
This is a geometric series, the same kind you met when a dynamic array doubles its capacity. The last term dominates: the sum is 2^(n+1) - 1, which is less than 2 × 2^n. Drop the constant and the total number of nodes is O(2^n). Each node is one function call that does one addition of two values, O(1) work. So the time complexity is O(2^n).
n-2 branch is shorter), so 2^n is an upper bound; the true growth is about 1.618^n, still exponential.| fibonacci(n), plain recursion | Cost | Why |
|---|---|---|
| Time | O(2^n) | up to 2^(n+1) - 1 calls, O(1) work each |
| Space | O(n) | the call stack is at most n deep at any moment (one root-to-leaf path), even though the tree has 2^n nodes |
Space is about the height of the tree, not its size. Only one root-to-leaf path is on the stack at a time; finished branches have been popped. NeetCode's article states the O(2^n) time; the O(n) space is the standard fact that goes with it.
4Four moves that make recursion routine
The lessons teach factorial and fibonacci. Interviews test whether you can set up any recursion cleanly, and whether you know when to leave it. These four moves cover that.
Move 1: Base case first (the checklist)
Before typing the recursive call, answer three questions in this order. Write the base case first, physically at the top of the function.
- What is the smallest input? Empty list,
n = 0,Nonenode,l >= r. Return its answer directly. This is the base case. - How do I make the input smaller by one step?
n - 1,i + 1,node.next,n // 2. Call yourself on that. Every call must move toward the base case. - Given the smaller answer, how do I build mine?
n * sub,nums[i] + sub,left + right. Trust that the recursive call returns the right thing. Do not trace it in your head; that is the whole point.
def sum_list(nums, i=0):
if i == len(nums): # 1. base case FIRST: nothing left to add
return 0
return nums[i] + sum_list(nums, i + 1) # 2. shrink 3. combine
sum_list([2, 3, 4, 1, 6]) # 16
sum_list([]) # 0 (the base case handles empty input for free)
Pass an index (i + 1), not a slice (nums[1:]). A slice copies the rest of the list on every call and turns O(n) into O(n²).
Move 2: Recursion to iteration
When the interviewer worries about stack depth, or when the recursion is one-branch and the answer is built on the way up, convert to a loop. Two recipes:
- One branch, combine on the way up (factorial, sum): keep the running answer in a variable and loop toward the base case. You saw this in section 2.
- Two branches (fibonacci): go bottom-up. Start from the base cases and build forward, keeping only the last two values. This is the seed of dynamic programming.
- Anything else: replace the call stack with your own list used as a stack. The trees lesson uses this for iterative traversals.
def fib_iter(n):
a, b = 0, 1 # a = fib(i), b = fib(i + 1), starting at i = 0
for _ in range(n):
a, b = b, a + b # slide the window one step forward
return a # O(n) time, O(1) space, no stack
def sum_list_iter(nums):
stack = [0] # our own stack replaces the call stack
total = 0
while stack:
i = stack.pop()
if i == len(nums): # the old base case
continue
total += nums[i]
stack.append(i + 1) # the old recursive call
return total
Move 3: Helper function with an accumulator
Sometimes the public function signature is fixed (LeetCode gives you reverseString(s)) but your recursion needs extra state: an index, two pointers, a running total. Write an inner helper that takes the extra arguments, and call it once. An accumulator is an argument that carries the answer-so-far down the calls, so nothing is left to do on the way up.
def factorial_acc(n):
def helper(n, acc):
if n <= 1:
return acc # the answer is already finished
return helper(n - 1, acc * n) # multiply BEFORE the call, not after
return helper(n, 1)
def reverseString(s): # LeetCode 344: reverse list s in place
def helper(l, r):
if l >= r: # pointers met or crossed: done
return
s[l], s[r] = s[r], s[l]
helper(l + 1, r - 1)
helper(0, len(s) - 1)
acc becomes res.Move 4: Memoization, a one-line preview
The fibonacci tree computed F(2) three times. Memoization means "remember each answer the first time, and look it up after that". In Python it is one decorator, and it turns O(2^n) into O(n) because each distinct n is computed once. The full treatment, including the bottom-up table version, is on the Dynamic Programming page.
from functools import lru_cache
@lru_cache(maxsize=None) # remembers every (n -> result) pair
def fib_memo(n):
if n <= 1:
return n
return fib_memo(n - 1) + fib_memo(n - 2)
fib_memo(50) # 12586269025, instantly. Plain recursion would take ~20 billion calls.
n from 0 to 5 is computed once (plain nodes), and the three repeated calls (orange rings) are answered from memory without opening their subtrees. Calls become O(n) and the stack is still O(n).5Big-O summary
Say the time and the space separately, and say where the space comes from ("the call stack") even when there is no data structure.
| Function | Shape | Time | Space | Why |
|---|---|---|---|---|
factorial(n) recursive | one branch | O(n) | O(n) | n calls, O(1) each; n frames on the stack at the peak |
factorial_iter(n) | loop | O(n) | O(1) | same n steps, two variables, no stack |
fibonacci(n) plain recursive | two branches | O(2^n) | O(n) | up to 2^(n+1) - 1 calls; stack depth is the tree height, n |
fib_memo(n) with lru_cache | two branches + cache | O(n) | O(n) | each n computed once; cache and stack both hold n entries |
fib_iter(n) bottom-up | loop | O(n) | O(1) | keeps only the last two values |
| General rule: one branch, depth d | O(d) | O(d) | a chain of d calls | |
| General rule: b branches, depth d | O(b^d) | O(d) | a tree with b^d leaves; only one path is on the stack at a time |
The last two rows are the ones to memorize. Time counts every node in the call tree. Space counts the height of that tree, because that is the most frames that are ever alive together.
6Mistakes to watch for
No if n <= 1: return, or a base case that the input can skip past (checking n == 0 when n can start negative). Result: RecursionError. Write the base case first, and make sure every recursive call moves toward it.
factorial(n - 1) on its own line does the work and throws the result away; the function returns None. Write return n * factorial(n - 1). Same for helpers: if the helper computes the answer, return it from the outer function too.
factorial returns 1 at the base case (the empty product). fibonacci returns n (0 for 0, 1 for 1). sum returns 0. Returning the wrong "nothing" value shifts every answer.
You will get lost by depth 3. Trust the recursive call: assume fibonacci(n - 1) is correct and only check that the base case and the combine step are right. Trace on paper only when debugging.
There is no list, but there is a call stack. One-branch recursion of depth n is O(n) space. Interviewers listen for this.
helper(nums[1:]) copies the rest of the list on every call: O(n²) time and memory. Pass an index i + 1 and keep one list.
If the same sub-problem appears twice in the tree (climbing stairs, fibonacci, grid paths), plain recursion is exponential. Add @lru_cache or a memo dictionary, or go bottom-up.
def fib(n, memo={}) works but the dictionary is shared across every call of the function forever, including different test cases. Prefer @lru_cache, or create the dictionary in the outer function and pass it to a helper.
A recursive walk over a 100,000-node linked list crosses Python's 1000-frame limit. For long chains use a loop; keep recursion for trees, where depth is usually log n.
Interview one-liners
- "A recursive function has a base case that answers directly and a recursive case that calls itself on a smaller input."
- "The sub-problems are solved in reverse order: the base case finishes first, the original call finishes last."
- "Recursion is not free in space: each call adds a frame to the call stack, so depth n means O(n) space."
- "One-branch recursion of depth n is O(n) time. Two-branch recursion of depth n is O(2^n) time because the call tree has about 2^n nodes; the last level dominates the geometric series."
- "Space is the height of the call tree, not its size; only one root-to-leaf path is on the stack at a time."
- "Plain recursive fibonacci recomputes the same sub-problems; memoization makes each n cost once, so O(n)."
- "Any recursion can be made iterative with an explicit stack; for one-branch cases a loop with an accumulator is enough."
7Practice, in this order
For each one, write the base case first, say the time and space out loud, then code it. Several of these have a loop solution; do the recursive version anyway, that is what this page trains.
| # | Problem | Move it trains | Level |
|---|---|---|---|
| 509 | Fibonacci Number | two-branch recursion, then add @lru_cache, then bottom-up loop | Easy |
| 1137 | N-th Tribonacci Number | three-branch recurrence; memo or loop with three variables | Easy |
| 70 | Climbing Stairs | fibonacci in disguise; recognize the recurrence f(n) = f(n-1) + f(n-2) | Easy |
| 344 | Reverse String (recursive version) | helper with two pointers l, r; base case l >= r | Easy |
| 231 | Power of Two | one-branch on n // 2; base cases n == 1 and odd n | Easy |
| 206 | Reverse Linked List (recursive version) | trust the call: reverse the rest, then fix two pointers on the way up | Easy |
| 21 | Merge Two Sorted Lists (recursive version) | base case: one list empty; pick the smaller head, recurse on the rest | Easy |
| 404 | Sum of Left Leaves | two-branch recursion on a tree; combine left + right | Easy |
| 24 | Swap Nodes in Pairs (recursive version) | one-branch on head.next.next; rewire on the way up | Medium |
| 50 | Pow(x, n) | halve the input: power(x, n // 2) squared, O(log n) depth; handle negative n | Medium |
The recursive reverse for 206, since it is the one people find hardest to picture:
def reverseList(head):
if head is None or head.next is None: # base case: empty or single node
return head
new_head = reverseList(head.next) # trust: reverses everything after head
head.next.next = head # the old "next" now points back at head
head.next = None # head becomes the tail
return new_head # same new head is passed all the way up
Your next step today: LeetCode 509. Write the plain recursive version, count the calls for n = 30 with a counter, then add @lru_cache and count again.
8Self-check
Answer out loud before opening. If you cannot answer in one breath, reread that section.
Q1What are the two parts of every recursive function, and what happens if one is missing?
The base case (answer directly, no call) and the recursive case (call yourself on a smaller input). Without a base case the calls never stop and the program crashes with a stack overflow; in Python, RecursionError.
Q2Why does the lesson say recursion solves the sub-problems "in reverse order"?
Because the biggest call starts first but finishes last. factorial(5) is the first call made and the last to return; factorial(1) is the last call made and the first to return. All the multiplication happens on the way back up.
Q3Trace it: factorial(4). List the calls in the order they are made, then the returns in the order they happen.
Calls: factorial(4), factorial(3), factorial(2), factorial(1). Returns: factorial(1) → 1, factorial(2) → 2 × 1 = 2, factorial(3) → 3 × 2 = 6, factorial(4) → 4 × 6 = 24.
Q4factorial creates no list or dictionary. Why is its space complexity O(n)?
Each call adds a frame to the call stack (the implicit stack the language keeps), and all n frames are alive at once when the base case runs. n frames → O(n) space.
Q5What is the time and space of the iterative factorial, and why is it better here?
O(n) time, O(1) space: a loop with two variables and no call stack. For a straight-line problem like factorial the loop is both simpler and cheaper. Recursion earns its keep on branching problems like trees.
Q6What does Python do at 1000 nested calls, and what can you change?
It raises RecursionError: maximum recursion depth exceeded. sys.setrecursionlimit(k) raises the limit. It helps a deep but correct recursion; it does not fix a missing base case, and for very large inputs a loop or explicit stack is the real fix.
Q7Write the recurrence relation for Fibonacci, including both base cases.
F(0) = 0, F(1) = 1, and F(n) = F(n-1) + F(n-2) for n ≥ 2. In code the base case is if n <= 1: return n.
Q8Trace it: fibonacci(4). How many calls are made in total, what is the deepest the stack gets, and which sub-problem is computed twice?
9 calls. The stack reaches 4 frames (F4, F3, F2, F1). F(2) is computed twice: once under F(3) and once directly under F(4). F(1) is also called three times. Result: 3.
Q9Explain, level by level, why two-branch recursion of depth n is O(2^n).
Level 0 has 1 node, level 1 has 2, level 2 has 4: each level can hold twice the previous, up to 2^n at level n. The total is the geometric series 1 + 2 + 4 + ... + 2^n = 2^(n+1) - 1. The last term dominates, so the node count is O(2^n), and each node is O(1) work.
Q10The fibonacci call tree has about 2^n nodes. Why is the space only O(n)?
Space is the maximum number of frames alive at one moment, which is one root-to-leaf path, of length at most n. Finished branches have already been popped off the stack.
Q11In one sentence, what does memoization change about the fibonacci tree?
Each distinct n is computed once and then served from a cache, so the repeated subtrees never run; calls drop from O(2^n) to O(n). Details on the DP page.
Q12What is an accumulator, and what is different about where the work happens?
An extra argument that carries the answer-so-far down into each call, like helper(n - 1, acc * n). The work happens on the way down, and the base case already holds the final answer, so the way up just passes it along. That shape converts to a loop directly.