AiTechWorlds
AiTechWorlds
Picture a set of Matryoshka dolls — the famous Russian nesting dolls. You hold the largest one in your hand. You open it. Inside is a slightly smaller doll. You open that one. Inside is another. And another. Until finally you reach the smallest doll, a solid piece of wood that contains nothing.
To find the innermost doll, you follow a pattern:
That's recursion. A problem defined in terms of a smaller version of itself, applied repeatedly until you reach a point simple enough to answer directly.
Every recursive algorithm has this same DNA. And understanding that DNA is the key to writing — and reading — recursive code fluently.
Every recursive function MUST have exactly these two things:
The base case is the simplest version of the problem, one you can answer directly without recursing further. It's the solid innermost doll.
Without a base case, the function calls itself forever — a stack overflow.
The recursive case breaks the current problem into a smaller version of the same problem, then calls itself on that smaller version.
The key rule: each recursive call must move closer to the base case.
When a function calls itself, Python (and every language) uses a call stack — a pile of active function frames.
factorial(4)
└── factorial(3)
└── factorial(2)
└── factorial(1)
└── returns 1 ← base case
returns 1 * 1 = 1
returns 2 * 1 = 2
returns 3 * 2 = 6
returns 4 * 6 = 24
Each frame holds that call's local variables and waits while inner calls resolve. When the base case returns, the stack unwinds — each frame uses its inner call's result to compute and return its own result.
def factorial(n):
# Base case: 0! = 1 and 1! = 1
if n <= 1:
return 1
# Recursive case: n! = n * (n-1)!
return n * factorial(n - 1)
print(factorial(5))
# Output: 120
Execution trace for factorial(5):
factorial(5) = 5 * factorial(4)
factorial(4) = 4 * factorial(3)
factorial(3) = 3 * factorial(2)
factorial(2) = 2 * factorial(1)
factorial(1) = 1 ← base case
= 2 * 1 = 2
= 3 * 2 = 6
= 4 * 6 = 24
= 5 * 24 = 120
Complexity: O(n) time, O(n) space (n stack frames)
def fib(n):
# Base cases
if n == 0: return 0
if n == 1: return 1
# Recursive case
return fib(n - 1) + fib(n - 2)
print(fib(6))
# Output: 8
# Sequence: 0, 1, 1, 2, 3, 5, 8, ...
WHY IT'S SLOW — the problem:
fib(5)
├── fib(4)
│ ├── fib(3)
│ │ ├── fib(2)
│ │ │ ├── fib(1) → 1
│ │ │ └── fib(0) → 0
│ │ └── fib(1) → 1
│ └── fib(2) ← REPEATED WORK
│ ├── fib(1) → 1
│ └── fib(0) → 0
└── fib(3) ← REPEATED AGAIN
├── fib(2)
│ ├── fib(1) → 1
│ └── fib(0) → 0
└── fib(1) → 1
fib(3) is computed twice, fib(2) is computed three times. For fib(50), this explodes to O(2ⁿ) — over a trillion operations. This motivates memoization (dynamic programming), covered in a later lesson.
def sum_list(arr):
# Base case: empty list sums to 0
if len(arr) == 0:
return 0
# Recursive case: first element + sum of rest
return arr[0] + sum_list(arr[1:])
print(sum_list([1, 2, 3, 4, 5]))
# Output: 15
Trace:
sum_list([1,2,3,4,5])
= 1 + sum_list([2,3,4,5])
= 1 + 2 + sum_list([3,4,5])
= 1 + 2 + 3 + sum_list([4,5])
= 1 + 2 + 3 + 4 + sum_list([5])
= 1 + 2 + 3 + 4 + 5 + sum_list([])
= 1 + 2 + 3 + 4 + 5 + 0 = 15
def power(base, exp):
# Base case: anything to the power 0 is 1
if exp == 0:
return 1
# Recursive case: base^exp = base * base^(exp-1)
return base * power(base, exp - 1)
print(power(2, 8))
# Output: 256
Bonus insight: A smarter version halves the exponent each step — base^exp = (base^(exp//2))² — reducing this to O(log n) time. This is called fast exponentiation.
def infinite_recursion(n):
return infinite_recursion(n + 1) # No base case!
infinite_recursion(0)
# RecursionError: maximum recursion depth exceeded
Python's default recursion limit is 1000 frames. Exceed it and your program crashes. This is why every recursive function must have a base case that is always reachable.
Common mistake — base case exists but is never reached:
def countdown(n):
if n == 0: return # base case
return countdown(n + 1) # Bug: goes UP, never reaches 0
| Aspect | Recursion | Iteration |
|---|---|---|
| Readability | Natural for tree/graph problems | Natural for sequential problems |
| Memory | Uses call stack (O(depth)) | Usually O(1) extra space |
| Speed | Function call overhead | Slightly faster in practice |
| Risk | Stack overflow on deep input | No stack risk |
| Best for | Trees, graphs, divide-and-conquer | Arrays, simple loops |
Any recursive function can be rewritten iteratively (using an explicit stack). Some are much cleaner recursively.
Recursion shines when the data structure itself is recursive:
Binary Tree: Directory system:
5 /home
/ \ ├── user/
3 8 │ ├── docs/
/ \ \ │ └── photos/
1 4 9 └── tmp/
Both a binary tree and a file system are defined as: a node containing smaller nodes of the same type. Recursive traversal writes itself almost automatically.
def tree_sum(node):
if node is None: # base case: empty node
return 0
return node.val + tree_sum(node.left) + tree_sum(node.right)
This would take a complex loop with an explicit stack to replicate iteratively.
When writing a recursive function, don't try to mentally trace every call. Instead:
For factorial(n):
if n <= 1: return 1factorial(n-1) correctly returns (n-1)!return n * factorial(n-1)Trust the recursion. The math handles the rest.
Next lesson: Backtracking — recursion with the power to undo decisions.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises