Skip to content

Memoization and Recursion

Memoization caches function results to avoid redundant computations. It transforms exponential-time recursive algorithms into polynomial-time solutions.


The Problem: Redundant Computation

def fibonacci(n):
    '''Naive fibonacci: exponential time O(2^n)'''
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# fibonacci(5) calls fibonacci(3) three times, fibonacci(2) five times!
# Time complexity: O(2^n)
import time
start = time.time()
result = fibonacci(35)
elapsed = time.time() - start
print(f"fibonacci(35) = {result}, took {elapsed:.2f} seconds")

lru_cache Decorator

from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci_cached(n):
    '''Memoized fibonacci: linear time O(n)'''
    if n <= 1:
        return n
    return fibonacci_cached(n - 1) + fibonacci_cached(n - 2)

# Now O(n) instead of O(2^n)!
import time
start = time.time()
result = fibonacci_cached(35)
elapsed = time.time() - start
print(f"fibonacci_cached(35) = {result}, took {elapsed:.6f} seconds")

# View cache statistics
print(fibonacci_cached.cache_info())

Manual Memoization

def fibonacci_memo(n, cache=None):
    '''Manual memoization with dictionary'''
    if cache is None:
        cache = {}

    if n in cache:
        return cache[n]

    if n <= 1:
        return n

    cache[n] = fibonacci_memo(n - 1, cache) + fibonacci_memo(n - 2, cache)
    return cache[n]

print(fibonacci_memo(30))  # Nearly instant

Example: Longest Common Subsequence

from functools import lru_cache

@lru_cache(maxsize=None)
def lcs(text1, text2):
    '''Longest common subsequence using memoization'''
    if not text1 or not text2:
        return 0

    if text1[0] == text2[0]:
        return 1 + lcs(text1[1:], text2[1:])

    return max(
        lcs(text1[1:], text2),
        lcs(text1, text2[1:])
    )

print(lcs("abcde", "ace"))     # 3
print(lcs.cache_info())         # Show cache hits/misses

Cache Strategies

from functools import lru_cache

# Limited cache size (evicts least recently used)
@lru_cache(maxsize=128)
def expensive_function(x):
    return x ** 2

# Unbounded cache (use with caution)
@lru_cache(maxsize=None)
def mathematical_function(n):
    return n * (n + 1) // 2

# Clear cache when needed
expensive_function.cache_clear()

Performance Impact

Memoization provides dramatic speedup for recursive algorithms with overlapping subproblems: - Without memoization: O(2^n) - With memoization: O(n)