In this post, we will explore different ways to calculate Fibonacci numbers in Python. We will start with a naive recursive approach, and then move to more efficient methods.
Naive Recursive Approach
The Fibonacci sequence is defined by the recurrence relation: \(F_n = F_{n-1} + F_{n-2}\) with seed values \(F_0=0\) and \(F_1=1\).
A naive implementation of this in Python is a direct translation of the mathematical formula:
This approach is very intuitive, but it is also very slow. For n=40
, it already takes a noticeable amount of time to compute. The reason for this is that the function calls itself twice for each n
, leading to an exponential growth of the call stack.
Visualizing the Call Stack
To better understand the problem, let’s visualize the call stack. We can create a “talkative” version of our function that logs every call.
from rich.console import Console
from rich.tree import Tree
def fibonacci_recursive_talkative(n: int) -> int:
"""Calculate the nth Fibonacci number using a recursive approach."""
tree = Tree(f"fibonacci_recursive_talkative({n})")
def worker(n: int, tree: Tree) -> int:
"""Calculate the nth Fibonacci number using a recursive approach."""
node = tree.add(f"fibonacci_recursive({n}) called")
if n < 0:
raise ValueError("Fibonacci is not defined for negative numbers.")
if n < 2:
return n
return worker(n - 1, node) + worker(n - 2, node)
result = worker(n, tree)
Console().print(tree)
return result
For n=6
, the call tree looks like this:
fibonacci_recursive_talkative(6) └── fibonacci_recursive(6) called ├── fibonacci_recursive(5) called │ ├── fibonacci_recursive(4) called │ │ ├── fibonacci_recursive(3) called │ │ │ ├── fibonacci_recursive(2) called │ │ │ │ ├── fibonacci_recursive(1) called │ │ │ │ └── fibonacci_recursive(0) called │ │ │ └── fibonacci_recursive(1) called │ │ └── fibonacci_recursive(2) called │ │ ├── fibonacci_recursive(1) called │ │ └── fibonacci_recursive(0) called │ └── fibonacci_recursive(3) called │ ├── fibonacci_recursive(2) called │ │ ├── fibonacci_recursive(1) called │ │ └── fibonacci_recursive(0) called │ └── fibonacci_recursive(1) called └── fibonacci_recursive(4) called ├── fibonacci_recursive(3) called │ ├── fibonacci_recursive(2) called │ │ ├── fibonacci_recursive(1) called │ │ └── fibonacci_recursive(0) called │ └── fibonacci_recursive(1) called └── fibonacci_recursive(2) called ├── fibonacci_recursive(1) called └── fibonacci_recursive(0) called
8
As you can see, many values are calculated multiple times. For example, fibonacci_recursive(4)
is calculated twice, fibonacci_recursive(3)
is calculated three times, and so on. This is the reason for the inefficiency.
Memoization
We can improve the performance by storing the results of the function calls in a cache (a dictionary). This technique is called memoization.
__CACHE: dict[int, int] = {}
def fibonacci_recursive_cached(n: int) -> int:
"""Calculate the nth Fibonacci number using recursion with memoization."""
if n < 0:
raise ValueError("Fibonacci is not defined for negative numbers.")
if n < 2:
return n
if n in __CACHE:
return __CACHE[n]
__CACHE[n] = fibonacci_recursive_cached(n - 1) + fibonacci_recursive_cached(n - 2)
return __CACHE[n]
This version is much faster. However, it will fail for large n
(e.g., n=8_000
) because of Python’s recursion depth limit.
Decorators for Caching
A more elegant way to implement memoization is by using a decorator. A decorator is a function that takes another function and extends its behavior without explicitly modifying it. Python’s functools
module provides a built-in decorator lru_cache
that does exactly what we need.
from functools import cache
@cache
def fibonacci_recursive_cached_decorator(n: int) -> int:
"""Calculate the nth Fibonacci number using recursion with memoization."""
if n < 0:
raise ValueError("Fibonacci is not defined for negative numbers.")
if n < 2:
return n
return fibonacci_recursive_cached_decorator(
n - 1
) + fibonacci_recursive_cached_decorator(n - 2)
This is the most concise and Pythonic way to write a cached recursive Fibonacci function.
Iterative Approach
Another way to solve the problem is to use an iterative approach. This is equivalent to tail recursion optimization.
This is the most efficient way (we’ll see)to calculate Fibonacci numbers in Python. It has a linear time complexity and constant space complexity.
Download the whole code here.