In this video lesson we will learn about memoization and use it to improve the recursive fibonacci algorithm written in the previous lesson:
Problem - Fibonacci:
The Fibonacci numbers, commonly denoted F(n)
form a sequence, called the Fibonacci sequence, such that each number is the sum of the two preceding ones, starting from 0
and 1
. That is,
F(0) = 0, F(1) = 1 F(n) = F(n - 1) + F(n - 2), for n > 1.
Write a recursive function with memoization that given n
, returns F(n)
.
Example 1:
Input: n = 2 Output: 1 Explanation: F(2) = F(1) + F(0) = 1 + 0 = 1.
Example 2:
Input: n = 3 Output: 2 Explanation: F(3) = F(2) + F(1) = 1 + 1 = 2.
Example 3:
Input: n = 4 Output: 3 Explanation: F(4) = F(3) + F(2) = 2 + 1 = 3.
0 <= n <= 30
Your algorithm should run in O(n) time and use O(n) extra space.
Memoization is a powerful optimization technique used to speed up recursive algorithms by storing the results of expensive function calls and reusing them when the same inputs occur again. This technique is particularly useful in dynamic programming and can significantly reduce the time complexity of algorithms.
One common scenario where memoization is beneficial is in computing the Fibonacci sequence. The naive recursive approach to calculate Fibonacci numbers has an exponential time complexity, making it inefficient for large inputs. By using memoization, we can improve the time complexity to linear, making the algorithm much more efficient.
The fundamental concept of memoization involves storing the results of function calls in a data structure (usually a dictionary or an array) and checking this data structure before making a new function call. If the result is already computed, it is returned immediately, avoiding redundant calculations.
Let's start with a simple example to illustrate the concept:
# Naive recursive approach to calculate Fibonacci numbers
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Example usage
print(fibonacci(5)) # Output: 5
In this naive approach, the function makes redundant calls, leading to an exponential time complexity. We can optimize this using memoization.
To implement memoization, we need to follow these steps:
Here is the modified Fibonacci function with memoization:
# Fibonacci with memoization
def fibonacci_memo(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)
return memo[n]
# Example usage
print(fibonacci_memo(5)) # Output: 5
In this implementation, we use a dictionary memo
to store the results of function calls. Before making a new call, we check if the result is already in the dictionary. If it is, we return it immediately. Otherwise, we compute the result, store it in the dictionary, and then return it.
Let's look at a few more examples to understand how memoization works in different contexts:
# Example 1: Fibonacci of 2
print(fibonacci_memo(2)) # Output: 1
# Example 2: Fibonacci of 3
print(fibonacci_memo(3)) # Output: 2
# Example 3: Fibonacci of 4
print(fibonacci_memo(4)) # Output: 3
In each of these examples, the function efficiently computes the Fibonacci number by reusing previously computed results, avoiding redundant calculations.
When using memoization, it's important to be aware of common pitfalls and follow best practices:
None
as the default value and initialize the data structure inside the function.Once you are comfortable with basic memoization, you can explore advanced techniques such as:
Here is the complete code implementation of the Fibonacci function with memoization:
# Fibonacci with memoization
def fibonacci_memo(n, memo=None):
# Initialize memo dictionary if not provided
if memo is None:
memo = {}
# Check if result is already computed
if n in memo:
return memo[n]
# Base cases
if n <= 1:
return n
# Compute the result and store it in memo
memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)
return memo[n]
# Example usage
print(fibonacci_memo(5)) # Output: 5
print(fibonacci_memo(10)) # Output: 55
When debugging and testing code with memoization, consider the following tips:
unittest
or pytest
to automate your tests and ensure your code works as expected.import unittest
class TestFibonacciMemo(unittest.TestCase):
def test_fibonacci_memo(self):
self.assertEqual(fibonacci_memo(0), 0)
self.assertEqual(fibonacci_memo(1), 1)
self.assertEqual(fibonacci_memo(2), 1)
self.assertEqual(fibonacci_memo(3), 2)
self.assertEqual(fibonacci_memo(4), 3)
self.assertEqual(fibonacci_memo(10), 55)
if __name__ == '__main__':
unittest.main()
When approaching problems related to memoization, consider the following strategies:
Memoization is a powerful technique that can significantly improve the efficiency of recursive algorithms. By storing and reusing results of expensive function calls, you can reduce time complexity and make your code more efficient. Mastering memoization and other dynamic programming techniques will help you tackle a wide range of problems in programming.
Keep practicing and exploring further applications of memoization to enhance your problem-solving skills.
For further reading and practice problems related to memoization, check out the following resources: