Memoization


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.

Constraints:
  • 0 <= n <= 30
Note:

Your algorithm should run in O(n) time and use O(n) extra space.


Introduction

Memoization is a powerful optimization technique used to speed up programs by storing the results of expensive function calls and reusing them when the same inputs occur again. This technique is particularly useful in recursive algorithms where the same subproblems are solved multiple times. One common example is the Fibonacci sequence, where a naive recursive approach can be highly inefficient.

Understanding the Basics

In a naive recursive implementation of the Fibonacci sequence, the same calculations are repeated multiple times, leading to an exponential time complexity. Memoization helps by storing the results of these calculations in a cache (usually a dictionary or an array) so that they can be reused, reducing the time complexity to linear.

Example:

F(5) = F(4) + F(3)
F(4) = F(3) + F(2)
F(3) = F(2) + F(1)
F(2) = F(1) + F(0)

Without memoization, F(3) and F(2) are calculated multiple times. With memoization, we store these results and reuse them.

Main Concepts

The key concept in memoization is to use a data structure to store the results of function calls. When the function is called again with the same arguments, we can return the stored result instead of recalculating it.

Steps to Implement Memoization:

  1. Create a cache to store the results of function calls.
  2. Modify the recursive function to check the cache before performing any calculations.
  3. If the result is in the cache, return it. Otherwise, calculate the result, store it in the cache, and then return it.

Examples and Use Cases

Let's implement the Fibonacci sequence using memoization in JavaScript:

// Create a cache to store the results of function calls
const memo = {};

function fibonacci(n) {
  // Check if the result is in the cache
  if (n in memo) {
    return memo[n];
  }
  
  // Base cases
  if (n === 0) {
    return 0;
  }
  if (n === 1) {
    return 1;
  }
  
  // Recursive case with memoization
  memo[n] = fibonacci(n - 1) + fibonacci(n - 2);
  return memo[n];
}

// Example usage
console.log(fibonacci(2)); // Output: 1
console.log(fibonacci(3)); // Output: 2
console.log(fibonacci(4)); // Output: 3

Common Pitfalls and Best Practices

One common mistake is not initializing the cache properly or not checking it before performing calculations. Always ensure that the cache is checked first to avoid unnecessary computations.

Best practices include using clear and descriptive variable names, and ensuring that the cache is only accessible within the scope of the function to avoid unintended side effects.

Advanced Techniques

For more advanced memoization, consider using a higher-order function to create memoized versions of any function. This can be particularly useful for more complex algorithms.

function memoize(fn) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args);
    if (key in cache) {
      return cache[key];
    }
    const result = fn(...args);
    cache[key] = result;
    return result;
  };
}

const memoizedFibonacci = memoize(function(n) {
  if (n === 0) return 0;
  if (n === 1) return 1;
  return memoizedFibonacci(n - 1) + memoizedFibonacci(n - 2);
});

console.log(memoizedFibonacci(2)); // Output: 1
console.log(memoizedFibonacci(3)); // Output: 2
console.log(memoizedFibonacci(4)); // Output: 3

Debugging and Testing

When debugging memoized functions, ensure that the cache is being accessed and updated correctly. Use console logs or debugging tools to inspect the cache at different stages of execution.

For testing, write test cases that cover both base cases and recursive cases. Ensure that the function returns correct results for a range of inputs.

Thinking and Problem-Solving Tips

When approaching problems that can benefit from memoization, identify the subproblems that are being solved multiple times. Think about how you can store and reuse the results of these subproblems to improve efficiency.

Practice by implementing memoization in different algorithms, such as dynamic programming problems, to gain a deeper understanding of its applications.

Conclusion

Memoization is a valuable technique for optimizing recursive algorithms by storing and reusing the results of expensive function calls. By understanding and applying memoization, you can significantly improve the performance of your programs.

Practice implementing memoization in various problems to master this technique and explore its applications in more complex algorithms.

Additional Resources