recursion
What Is Recursion?
- Recursion is solving a problem by breaking it down to smaller instances of the same problem
- Factorial function:
n! = n * (n-1)!
- Fibonacci sequence:
f(n) = f(n-1) + f(n-2)
- In nature, they are like a tree
- Factorial function:
- In programming, recursion is a concept where a function calls itself
- Applications: Sorting algorithms (merge sort, quick sort), graph algorithms (DFS), etc.
Implementation
- Base case: Stopping condition, allows recursion to terminate
- Recursive case: breaks problem into smaller ones
Factorial
Code
def factorial(n: int) -> int:
# base case
if n == 0:
return 1
# recursion
return n * factorial(n - 1)
How it works with factorial(3)
Call
3 * factorial(2) => It pauses the current work and goes to calculate factorial(2)
2 * factorial(1) => It pauses the current work and goes to calculate factorial(1)
1 * factorial(0) => It pauses the current work and goes to calculate factorial(0)
factorial(0) returns 1 (Base Case)
Return
factorial(1) = 1 * 1 = 1
factorial(2) = 2 * 1 = 2
factorial(3) = 3 * 2 = 6
Complexity
- Finding a tight bound of recursive algorithm is hard
- Can be estimated based on number of calls to the method
- Number of calls:
O(n)
- Each call:
O(1)
time,O(1)
memory - Total:
O(n)
time,O(n)
memory
How to calculate time complexity
- For
factorial(n)
, the tree structure looks like this- Nodes: Each function call is a node. The initial call is the root
- Edges: Each recursive call creates a child node, so an edge represents the invocation from a caller to a callee
- Work per node: We need to determine the amount of work done at each node, excluding the recursive calls themselves
- Calculating time complexity
- Work at each node: In each call to
factorial(n)
, we perform two main operations- One comparison (
n == 0
) - One multiplication (
n * ...
) - These operations take a constant amount of time. Let's denote this constant work as
c
- One comparison (
- Depth of the tree: The recursion continues until
n
becomes 0. The initial call isfactorial(n)
, thenfactorial(n-1)
, and so on, down tofactorial(0)
. This creates a total ofn + 1
levels or a depth ofn
- Total work: To find the total time complexity, we sum the work done at each node in the tree
factorial(n)
doesc
workfactorial(n-1)
doesc
work- ...
factorial(1)
doesc
workfactorial(0)
doesc
work (just the comparison)
- Since there are
n + 1
nodes in our "tree" (which is essentially a single branch), the total work is the sum of the work at each level- Total work =
Σ_{i=0}^{n} c = c * (n + 1)
O(c * (n + 1)) = O(n)
- Total work =
- Work at each node: In each call to
How to calculate the space complexity
- For a recursive function, the primary space cost comes from the call stack. Each time the function calls itself, a new stack frame is pushed onto the stack. This frame stores the function's state, like the value of its local variables (in this case,
n
) and the return address. The space complexity is determined by the maximum number of frames on the stack at any single point in time - Let's trace the call stack for
factorial(4)
factorial(4)
is called. The stack has one frame[ factorial(4) ]
factorial(4)
callsfactorial(3)
. The stack grows[ factorial(4), factorial(3) ]
- This continues until the base case is reached. The stack is at its deepest when
factorial(0)
is called[ factorial(4), factorial(3), factorial(2), factorial(1), factorial(0) ]
- At its peak, the call stack holds
n + 1
frames (fromn
down to0
). Oncefactorial(0)
returns, the stack unwinds, and frames are popped off - Since the maximum memory required is proportional to the number of stack frames, and that number is
n + 1
, we simplify this toO(n)
in Big O notation. This means the space required grows linearly with the input valuen
Fibonacci
Code
def fibonacci(n: int) -> int:
# base case
if n < 2:
return n
# recursion
return fibonacci(n - 1) + fibonacci(n - 2)
How it works with fibonacci(4)
(in order)
Call
fibonacci(4) = fibonacci(3) + fibonacci(2)
fibonacci(3) = fibonacci(2) + fibonacci(1)
fibonacci(2) = fibonacci(1) + fibonacci(0)
fibonacci(1) returns 1 (Base Case)
fibonacci(0) returns 0 (Base Case)
fibonacci(1) returns 1 (Base Case)
fibonacci(2) = fibonacci(1) + fibonacci(0)
fibonacci(1) returns 1 (Base Case)
fibonacci(0) returns 0 (Base Case)
Return
fibonacci(2) = 1 + 0 = 1
fibonacci(3) = 1 + 1 = 2
fibonacci(2) = 1 + 0 = 1
fibonacci(4) = 2 + 1 = 3
- First,
fibonacci(3) = fibonacci(4) -> fibonacci(3) -> fibonacci(2) -> fibonacci(1)
andfibonacci(0)
- Then,
fibonacci(2) = fibonacci(2) -> fibonacci(1)
andfibonacci(0)
How to calculate time complexity
- Work at each Node: Just like with factorial, each
fibonacci
call does a constant amount of work (one comparison and one addition). Let's consider the cost of each node to be1
- Total work: The total time complexity is the total number of nodes in the tree. To find this, we can analyze the tree's structure
- Depth: The tree has a depth of
n
. The longest path follows thefib(n-1)
calls (e.g.,fib(4) -> fib(3) -> fib(2) -> fib(1)
) - Nodes per Level: At each level
k
, there are roughly2^k
nodes. This isn't perfectly accurate because the tree is "lopsided" (the right side is always shorter than the left), but it provides a good upper bound
- Depth: The tree has a depth of
- Summing the work: The total number of nodes is approximately the sum of nodes at each level, which is roughly:
2^0 + 2^1 + 2^2 + ... + 2^{n-1}
- This is a geometric series that sums to
2^n - 1
- This is a geometric series that sums to
- In Big O notation, we drop the constant
-1
, so the time complexity is:O(2^n)
How to calculate space complexity
- While the time complexity is exponential (
O(2^n)
) due to the total number of calls, the space complexity is determined by the maximum depth of the call stack at any single moment, not the total number of nodes in the recursion tree - Let's trace the execution for
fibonacci(4)
to see how the call stack behaves. The execution will proceed down the "leftmost" branch of the tree firstfibonacci(4)
is called- Stack:
[ fib(4) ]
- Stack:
- It calls
fibonacci(3)
- Stack:
[ fib(4), fib(3) ]
- Stack:
fibonacci(3)
callsfibonacci(2)
- Stack:
[ fib(4), fib(3), fib(2) ]
- Stack:
fibonacci(2)
callsfibonacci(1)
- Stack:
[ fib(4), fib(3), fib(2), fib(1) ]
- Stack:
- This is the deepest point the call stack will reach for the
n=4
example. The stack has a depth ofn
fibonacci(1)
returns. Its frame is popped off the stack- Stack:
[ fib(4), fib(3), fib(2) ]
- Stack:
- Now,
fibonacci(2)
callsfibonacci(0)
. The stack grows again but does not exceed its previous maximum depth- Stack:
[ fib(4), fib(3), fib(2), fib(0) ]
- Stack:
- The key insight is that the
fibonacci(n-2)
branch is only explored after thefibonacci(n-1)
call has returned and its stack frames have been popped. Therefore, the maximum number of frames on the stack at any one time is determined by the depth of the longest path in the recursion tree, which isn
- Because the maximum depth of the call stack is directly proportional to the input
n
, the space complexity isO(n)
Recursive vs. Iterative
Recursive | Iterative |
---|---|
Function calls itself, until base case is reached | Loop until end condition is met |
Tends to be more memory-intensive due to the call stack | More memory-efficient |
More overhead due to function calls | No overhead from function calls |
(Usually) More concise, elegant, mathy code | Easier to debug |
Suitable for problems that can be naturally broken down into subproblems (sorting) or trying all possibilities (backtracking) | Suitable for problems requiring sequential operations |
Base Knowledge
Factorial Function
Meaning
- The factorial of a natural number
n
(denoted asn!
) is the product of all positive integers from 1 up ton
- Example:
5! = 5 * 4 * 3 * 2 * 1 = 120
- Example:
Explaining the formula n! = n * (n-1)!
- This formula describes the factorial recursively. Let's break it down with the
5!
example- According to the formula,
5! = 5 * (5-1)! = 5 * 4!
- Similarly, to calculate
4!
, we apply the formula again:4! = 4 * (4-1)! = 4 * 3!
- This continues as follows:
3! = 3 * 2!
and2! = 2 * 1!
and1! = 1 * 0!
- According to the formula,
- This process stops at a point called the "base case". For factorials, the base case is defined as:
0! = 1
As you can see, the formula n! = n * (n-1)!
is a concise and elegant way to define the calculation of any factorial by reducing it to the problem of calculating the factorial of a smaller number
Fibonacci Sequence
Meaning
- The Fibonacci sequence is an infinite series of numbers where each number (from the third one onwards) is the sum of the two preceding ones
- The sequence most commonly starts with
0
and1
(or sometimes1
and1
). By the most common convention, the sequence is:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
1 = 0 + 1
and2 = 1 + 1
and3 = 1 + 2
and5 = 2 + 3
, and so on
Explaining the formula f(n) = f(n-1) + f(n-2)
- This formula defines the
n
-th Fibonacci number (denoted asf(n)
) based on the two numbers that come directly before it,f(n-1)
andf(n-2)
- For this formula to work, it also needs "base cases". These are the first two numbers in the sequence:
f(0) = 0
andf(1) = 1
- Now, let's use the formula to find a few numbers in the sequence
f(2): f(2) = f(1) + f(0) = 1 + 0 = 1
f(3): f(3) = f(2) + f(1) = 1 + 1 = 2
f(4): f(4) = f(3) + f(2) = 2 + 1 = 3
f(5): f(5) = f(4) + f(3) = 3 + 2 = 5
The Fibonacci sequence appears frequently in nature, such as in the arrangement of flower petals, the branching of trees, or the spiral patterns of seashells