Memoisation: Caching Recursive Results
Apply @functools.lru_cache and manual memo dicts to Fibonacci and climbing-stairs to eliminate exponential recomputation.
Memoisation: Caching Recursive Results is a free DSA Interview Prep lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DSA Interview Prep learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
The Problem with Redundant Recursion
Naive recursive Fibonacci computes the same values repeatedly. fib(5) calls fib(4) and fib(3); fib(4) calls fib(3) and fib(2) — so fib(3) is computed twice. This redundancy grows exponentially: fib(40) makes over one billion function calls. Memoisation solves this by storing each result the first time it is computed, so subsequent calls retrieve it in O(1) rather than recomputing it.
# Count calls without memoisation
call_count = [0]
def fib_plain(n):
call_count[0] += 1
if n <= 1: return n
return fib_plain(n-1) + fib_plain(n-2)
fib_plain(20)
print(f'fib(20) without memo: {call_count[0]:,} calls')
# ~21,891 calls for n=20; ~1 billion for n=40Manual Memoisation with a Dict
Add a memo dict as a parameter (or a closure). Before computing, check if the answer is already in memo. If yes, return it immediately. If no, compute it, store in memo, and return. Every unique sub-problem is now computed exactly once, turning O(2^n) into O(n) time and O(n) space for the memo dict plus O(n) stack space.
def fib_memo(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
return memo[n]
print(fib_memo(10)) # 55
print(fib_memo(50)) # 12586269025
print(fib_memo(100)) # huge number — still fast!functools.lru_cache Decorator
Python provides @functools.lru_cache(maxsize=None) (also available as @functools.cache in Python 3.9+) to automate memoisation. Adding this decorator above a function caches all calls by their arguments. maxsize=None means unlimited cache size — every unique argument combination is cached. This converts any recursive function into a memoised version with one line of code.
import functools
@functools.lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
print(fib(50)) # 12586269025
print(fib(100)) # 354224848179261915075
print(fib.cache_info()) # CacheInfo(hits=..., misses=..., maxsize=None, currsize=...)Climbing Stairs (LeetCode 70)
LeetCode 70 'Climbing Stairs': you can climb 1 or 2 steps at a time. How many ways to reach step n? This is Fibonacci in disguise: ways(n) = ways(n-1) + ways(n-2). Base cases: ways(0) = 1 (one way to stay at ground), ways(1) = 1. With memoisation, O(n) time and O(n) space.
import functools
@functools.lru_cache(maxsize=None)
def climbStairs(n):
if n <= 1:
return 1
return climbStairs(n-1) + climbStairs(n-2)
for i in range(1, 8):
print(f'climbStairs({i}) = {climbStairs(i)}')
# 1,2,3,5,8,13,21Coin Change (LeetCode 322)
LeetCode 322 'Coin Change': given denominations and a target amount, find the minimum number of coins. Top-down memoised recursion: dp(amount) = 1 + min(dp(amount - coin)) for each valid coin. The base case: dp(0) = 0. Cache each sub-amount. If a sub-amount is impossible, return infinity. Memoisation turns the exponential brute force into O(amount × len(coins)) time.
import functools
def coinChange(coins, amount):
@functools.lru_cache(maxsize=None)
def dp(rem):
if rem == 0:
return 0
if rem < 0:
return float('inf')
return 1 + min(dp(rem - c) for c in coins)
result = dp(amount)
return result if result != float('inf') else -1
print(coinChange([1, 5, 11], 15)) # 3 (5+5+5)
print(coinChange([1, 2, 5], 11)) # 3 (5+5+1)
print(coinChange([2], 3)) # -1Word Break (LeetCode 139) with Memo
LeetCode 139 'Word Break': determine if a string can be segmented into dictionary words. Top-down recursion: can_break(s, start) tries every prefix s[start:end]; if it is in the dictionary and can_break(s, end) is true, return true. Without memo this is O(2^n); with memo (caching each start index) it becomes O(n² × L) where L is the max word length.
import functools
def wordBreak(s, wordDict):
word_set = set(wordDict)
@functools.lru_cache(maxsize=None)
def can_break(start):
if start == len(s):
return True
for end in range(start + 1, len(s) + 1):
if s[start:end] in word_set and can_break(end):
return True
return False
return can_break(0)
print(wordBreak('leetcode', ['leet', 'code'])) # True
print(wordBreak('applepenapple', ['apple','pen'])) # True
print(wordBreak('catsandog', ['cats','dog','sand','and','cat'])) # FalseMemoisation vs Tabulation
Memoisation (top-down) starts with the original problem and caches answers as they are discovered recursively. It solves only the sub-problems that are actually needed. Tabulation (bottom-up) pre-fills a table from small sub-problems to large, solving all sub-problems regardless. Memoisation is easier to derive from a recursive solution; tabulation avoids recursion-depth limitations and function-call overhead.
# Memoisation (top-down)
import functools
@functools.lru_cache(maxsize=None)
def fib_td(n):
if n <= 1: return n
return fib_td(n-1) + fib_td(n-2)
# Tabulation (bottom-up)
def fib_bu(n):
if n <= 1: return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
print(fib_td(20), fib_bu(20)) # 6765 6765
# Both O(n) time; fib_bu avoids recursion limitSpace Optimisation: Rolling Variables
Many DP problems that memoised recursion solves in O(n) space can be further optimised to O(1) space when only a fixed number of previous sub-problem answers are needed. For Fibonacci: only the last two values matter. For climbing stairs: same. Rolling two variables replaces the entire memo dict or table.
# Fibonacci with O(1) space
def fib_o1(n):
if n <= 1:
return n
prev2, prev1 = 0, 1
for _ in range(2, n + 1):
prev2, prev1 = prev1, prev2 + prev1
return prev1
for i in range(8):
print(f'fib({i})={fib_o1(i)}', end=' ')
print()
# Climbing stairs O(1) space
def climbStairs_o1(n):
if n <= 1: return 1
a, b = 1, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
print(climbStairs_o1(10)) # 89lru_cache vs Closure vs Global Dict
Three ways to implement memoisation manually. A global dict is simple but pollutes module scope. A closure encapsulates the cache within the function, preventing leakage but requiring a wrapper. @lru_cache is the cleanest — one decorator replaces all boilerplate. In an interview context, start with @lru_cache unless the interviewer specifically asks for a manual implementation.
import functools
# 1. Global dict (messy)
memo_global = {}
def fib_global(n):
if n in memo_global: return memo_global[n]
if n <= 1: return n
memo_global[n] = fib_global(n-1) + fib_global(n-2)
return memo_global[n]
# 2. Closure (cleaner scope)
def make_fib():
cache = {}
def fib(n):
if n in cache: return cache[n]
if n <= 1: return n
cache[n] = fib(n-1) + fib(n-2)
return cache[n]
return fib
fib_closure = make_fib()
# 3. lru_cache (best)
@functools.lru_cache(maxsize=None)
def fib_cached(n):
if n <= 1: return n
return fib_cached(n-1) + fib_cached(n-2)
print(fib_global(30), fib_closure(30), fib_cached(30)) # all 832040When Memoisation Does Not Help
Memoisation only accelerates problems with overlapping sub-problems — cases where the same sub-problem is computed multiple times. If every sub-problem is unique (like in simple tree traversal where each node is visited exactly once), memoisation adds overhead without benefit. Also, memoisation cannot fix problems where the recursive tree is exponential in the number of distinct sub-problems rather than in reuse — those require a different algorithm altogether.
# Memoisation DOES help: overlapping sub-problems (Fibonacci)
# fib(n) reuses fib(n-2), fib(n-3), etc.
# Memoisation does NOT help: distinct sub-problems (permutations)
# Each unique (remaining_elements, target) pair is truly distinct
# The exponential complexity comes from the state space itself
print('Memoisation: useful when SAME sub-problem recurs multiple times')
print('Not useful: when every sub-problem is unique to one recursive path')Summary: Memoisation Checklist
Apply memoisation when: you have a recursive solution that is correct but slow due to redundant recomputation, the function has a small number of distinct argument combinations, and the return value depends only on the arguments (pure function — no side effects, no global state). Check the sub-problem state space: if there are at most O(n) or O(n²) distinct states, memoisation converts exponential to polynomial time.
Quick Check
Test your understanding of Data Structures & Algorithms — Coding Interview Prep concepts from this lesson.
Lesson Recap
In this lesson you learned: memoisation stores results of sub-problems to avoid recomputation, converting exponential recursion to polynomial time, @functools.lru_cache is the idiomatic Python tool requiring only one line, and memoisation (top-down) and tabulation (bottom-up) are the two styles of DP — memoisation is easier to derive, tabulation avoids stack depth issues. Congratulations — you have completed the recursion and hash-map modules!
Frequently asked questions
Is the “Memoisation: Caching Recursive Results” lesson free?
Yes — the full text of “Memoisation: Caching Recursive Results” is free to read here on the web, and the DSA Interview Prep course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DSA Interview Prep course, upgrade to CoddyKit PRO.
What will I learn in “Memoisation: Caching Recursive Results”?
Apply @functools.lru_cache and manual memo dicts to Fibonacci and climbing-stairs to eliminate exponential recomputation. You practise DSA Interview Prep with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start DSA Interview Prep?
No prior experience is required. DSA Interview Prep on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Memoisation: Caching Recursive Results” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this DSA Interview Prep lesson?
Yes. Every DSA Interview Prep lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Recursion Framework: Base Case, Trust, Build
- Visualising the Call Stack
- Recursive vs Iterative Trade-offs
- Memoisation: Caching Recursive Results