Hello! Welcome to the final lesson in our module on Recursion and Backtracking.
In our previous lessons, we've seen how recursion is a powerful tool for exploring solution spaces, like generating permutations or finding a path through a grid. In those problems, each recursive path was mostly unique. However, many recursive algorithms suffer from a major inefficiency: they solve the exact same subproblem over and over again.
Today, we will address this by introducing memoization, a powerful optimization technique. You will learn how to identify problems with overlapping subproblems and apply memoization to drastically improve their performance. This concept is the cornerstone of top-down Dynamic Programming (DP), a critical topic for any technical interview.
1. The Problem: Overlapping Subproblems
Let's start with a classic example that beautifully illustrates the problem: calculating the n-th Fibonacci number. The rule is F(n) = F(n-1) + F(n-2), with base cases F(0) = 0 and F(1) = 1.
A direct translation of this into a recursive function is simple, but it hides a massive inefficiency. Let's watch a video that visualizes what's really happening.
Dynamic Programming - Top Down Memoization & Bottom Up Tabulation - DSA Course in Python Lecture 15
The video 'Dynamic Programming - Top Down Memoization & Bottom Up Tabulation' by Greg Hogg provides a clear illustration of this issue. We will focus on the part that explains the naive recursive solution and its pitfalls.
Watch from 01:31 to 07:13. Pay close attention to the recursion tree diagram for F(6). Notice how many times the same function, like F(4) or F(3), is called. This is the definition of 'overlapping subproblems'.
As you saw, to compute fib(6), the algorithm computes fib(4) twice, fib(3) three times, and so on. This redundancy leads to an exponential time complexity of roughly , which is extremely slow for even moderately large values of n. For example, fib(40) would involve billions of function calls.
This is a huge red flag in an interview. If you identify that your recursive solution has overlapping subproblems, you must optimize it.
2. The Solution: Memoization
The core idea behind optimization is simple: if we've already calculated fib(3), why should we ever calculate it again? We can just "remember" the result. This technique of storing the results of expensive function calls and returning the cached result when the same inputs occur again is called memoization.
Memoization is a form of caching. Given your experience with technologies like Redis and RAG chatbots, you can think of it as an in-memory, function-level cache. We typically use a hash map (a dictionary in Python) for this cache.
The logic works like this:
- Check the cache: Before doing any computation, check if the result for the current input (e.g.,
n) is already in our cache. - Cache Hit: If it is, return the value from the cache immediately. This is an lookup.
- Cache Miss: If it's not, perform the expensive computation.
- Populate the cache: Before returning the newly computed value, store it in the cache for future use.
The following image visually demonstrates how memoization prunes the recursion tree for fib(7). The green nodes represent calls that are skipped because the result was retrieved from the cache.

Now, let's see how this is implemented in code and how it changes the complexity.
Dynamic Programming - Top Down Memoization & Bottom Up Tabulation - DSA Course in Python Lecture 15
Let's return to Greg Hogg's video, where he codes the memoized solution and explains the dramatic performance improvement.
Watch from 07:13 to 13:07. This section explains how to use a dictionary as a cache (memo) and integrates it into the recursive function. He then analyzes the new time and space complexity.
Complexity Analysis with Memoization
-
Time Complexity:
Each Fibonacci numberfib(0), fib(1), ..., fib(n)is computed only once. Subsequent calls are lookups. This transforms the algorithm from exponential to linear time. -
Space Complexity:
This is a crucial point that often trips people up. Space is required for two things:- The memoization cache, which will store
n+1key-value pairs in the worst case: . - The recursion call stack. The maximum depth of the recursion is
n. For example, to computefib(n), you'll callfib(n-1), thenfib(n-2), and so on, down to the base cases. This creates a chain ofncalls on the stack: .
- The memoization cache, which will store
The video below from HackerRank, featuring Gayle Laakmann McDowell (author of Cracking the Coding Interview), offers a particularly clear explanation of space complexity for both the naive and memoized versions.
Algorithms: Memoization and Dynamic Programming
This short clip clarifies a common misconception about the space complexity of the naive recursive solution and correctly analyzes it for both versions.
Watch from 02:52 to 04:18. Note her clear distinction between the total number of calls over time and the maximum number of calls on the stack at any single point in time.
Test your understanding!
Imagine a problem where you can climb a staircase by taking either 1, 2, or 3 steps at a time. A recursive function ways(n) calculates the number of ways to reach the n-th step, defined as ways(n) = ways(n-1) + ways(n-2) + ways(n-3).
If you were to memoize this function, what would be the time and space complexity of your optimized solution?
Show answer
The complexity would be identical to the Fibonacci example.
- Time Complexity: . You would compute
ways(i)for eachifrom the base cases up tononly once. - Space Complexity: . You'd need space for the cache to store the results and space for the recursion call stack depth.
3. Memoization in a Broader Context
Memoization is not just a random trick; it's the core of a powerful algorithmic paradigm called top-down dynamic programming. Let's formalize the conditions needed to apply it.
Dynamic Programming Interview Questions & Tips
The article 'Dynamic Programming Interview Questions & Tips' formally defines the two properties a problem must have to be solvable with dynamic programming.
Read the section titled 'Optimal Substructure and Overlapping Subproblems'. These are the two key properties you should explicitly mention in an interview when justifying a DP approach.
In summary, for memoization to work, a problem must exhibit:
- Overlapping Subproblems: The same subproblems are solved multiple times.
- Optimal Substructure: The optimal solution to the main problem can be constructed from the optimal solutions of its subproblems.
There's one more important prerequisite for a function to be memoizable. Let's explore it in the following reading.
Chapter 7 - Memoization and Dynamic Programming
The article 'Chapter 7 - Memoization and Dynamic Programming' from the book 'Recursion According to Al Sweigart' provides an excellent discussion on a key requirement for memoization: pure functions.
First, read the section 'Memoization in Functional Programming' to understand what pure functions are (deterministic, no side effects). Then, read 'What Happens When You Memoize Impure Functions?' to see why this is a critical requirement.
Because memoization works by skipping a function's execution, it should only be used on pure functions—those that are deterministic (always return the same output for the same input) and have no side effects (don't modify external state, print to the console, write to a file, etc.). Our fib(n) function is a perfect example of a pure function.
A Practical Shortcut in Python: @lru_cache
Manually implementing the cache dictionary is great for understanding, but in a real-world or interview setting, Python offers a much cleaner way to do this using a decorator.
Chapter 7 - Memoization and Dynamic Programming
Let's look at the same article again to learn about a built-in Python decorator that handles memoization for you.
Read the section 'Python’s functools Module'. This introduces the @lru_cache decorator, which is the standard, production-grade way to memoize a function in Python. It's a great tool to have in your toolbox.
Using @lru_cache, our highly optimized Fibonacci function becomes incredibly concise:
from functools import lru_cache
@lru_cache(maxsize=None) # maxsize=None makes the cache unlimited
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
# This will now run instantly, even for large n.
print(fib(50))
Using this decorator shows an interviewer that you are aware of standard library tools and best practices.
Conclusion
Today you learned one of the most important optimization techniques in algorithms. By identifying and eliminating the redundant work caused by overlapping subproblems, you can turn an infeasible exponential-time solution into a highly efficient linear-time one.
Key Takeaways:
- Naive recursive solutions can be extremely inefficient if they solve the same overlapping subproblems repeatedly.
- Memoization is a caching technique used to store the results of expensive function calls to avoid re-computation.
- It is the central idea behind top-down dynamic programming.
- The typical implementation uses a hash map to store results, checking it before computation and populating it after.
- Memoization reduces the time complexity of problems like Fibonacci from to , at the cost of space for the cache and recursion stack.
- In Python, you can easily implement memoization using the
@lru_cachedecorator from thefunctoolsmodule.
This lesson serves as a perfect bridge to our next module, Dynamic Programming. Having mastered top-down DP (recursion + memoization), you are now ready to tackle the other side of the coin. In our next lesson, we will learn how to convert a memoized recursive solution into an iterative, bottom-up DP solution, often called tabulation. This approach avoids recursion altogether and can be even more efficient.