Skip to content

Memoization

Memoization is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again. It’s a form of dynamic programming where computed solutions to subproblems are stored and reused. The core idea is to avoid redundant computations by remembering the results of previous calculations.

Why is it important?

Memoization is crucial for solving problems exhibiting overlapping subproblems – situations where the same subproblems are encountered multiple times during the computation of a larger problem. Without memoization, these subproblems would be recalculated repeatedly, leading to significant performance degradation, especially for larger inputs. This is particularly relevant in recursive algorithms.

Core Concepts and Terminology:

  • Cache: A data structure (typically a hash table or dictionary) used to store the results of previous function calls. The key is usually the function’s input(s), and the value is the corresponding output.
  • Lookup: The process of checking if a result for a given input already exists in the cache.
  • Overlapping Subproblems: A characteristic of problems where the same subproblems are solved multiple times during the computation.
  • Dynamic Programming: A broader algorithmic paradigm that often employs memoization to optimize solutions.

2. When to Use Memoization (and When Not To)

Section titled “2. When to Use Memoization (and When Not To)”

Use Memoization when:

  • You have a recursive function.
  • The function’s input space is relatively small (otherwise, the cache can become too large).
  • The function has overlapping subproblems – the same inputs are used repeatedly.
  • The cost of computing the function is relatively high compared to the cost of looking up the result in a cache.

Don’t Use Memoization when:

  • The input space is extremely large, making the cache impractical.
  • The function is very cheap to compute. The overhead of managing the cache outweighs the performance gains.
  • The function has side effects (modifies global state), as caching results might lead to unexpected behavior.
  • The problem can be solved more efficiently with a different algorithm (e.g., iterative approach).

3. Core Algorithm / Data Structure Template

Section titled “3. Core Algorithm / Data Structure Template”
  1. Initialize a cache: Create a data structure (e.g., a dictionary or hash map) to store the results of function calls.
  2. Check the cache: Before computing a result, check if it’s already present in the cache. If it is, return the cached value.
  3. Compute the result: If the result is not in the cache, compute it using the original function.
  4. Store the result: Store the computed result in the cache, using the input as the key and the output as the value.
  5. Return the result: Return the computed (or cached) result.
def fibonacci_memoization(n, memo={}):
"""
Calculates the nth Fibonacci number using memoization.
"""
if n in memo:
return memo[n]
if n <= 1:
return n
else:
result = fibonacci_memoization(n-1, memo) + fibonacci_memoization(n-2, memo)
memo[n] = result
return result
import java.util.HashMap;
import java.util.Map;
public class FibonacciMemoization {
private static Map<Integer, Integer> memo = new HashMap<>();
public static int fibonacci(int n) {
if (memo.containsKey(n)) {
return memo.get(n);
}
if (n <= 1) {
return n;
} else {
int result = fibonacci(n - 1) + fibonacci(n - 2);
memo.put(n, result);
return result;
}
}
public static void main(String[] args) {
System.out.println(fibonacci(6)); //Example usage
}
}
#include <iostream>
#include <map>
using namespace std;
map<int, long long> memo;
long long fibonacci(int n) {
if (memo.count(n)) {
return memo[n];
}
if (n <= 1) {
return n;
} else {
long long result = fibonacci(n - 1) + fibonacci(n - 2);
memo[n] = result;
return result;
}
}
int main() {
cout << fibonacci(6) << endl; // Example usage
return 0;
}
OperationTime Complexity (Best/Average/Worst)Space Complexity
Fibonacci (Memoized)O(n) / O(n) / O(n)O(n)
Cache LookupO(1)-
Cache InsertionO(1)-
  • Time Complexity: Without memoization, the Fibonacci sequence has exponential time complexity due to repeated calculations. Memoization reduces it to linear time because each subproblem is solved only once.
  • Space Complexity: The space complexity is determined by the size of the cache, which is proportional to the number of distinct function calls (n in the Fibonacci example).

Pro Tips:

  • Choose the right cache: Hash tables/dictionaries offer O(1) average-case lookup, insertion, and deletion, making them ideal for most memoization scenarios.
  • Consider cache eviction policies (for large caches): If the cache size is limited, you might need strategies like LRU (Least Recently Used) to manage cache space efficiently.
  • Functional Programming: Memoization integrates well with functional programming paradigms, where immutability and pure functions are emphasized.

Common Pitfalls:

  • Incorrect cache key: Using an inappropriate key can lead to incorrect results or cache misses. Ensure the key uniquely identifies the function’s input.
  • Mutable arguments: If the function’s arguments are mutable objects, changes to these objects might invalidate the cached results.
  • Over-engineering: Don’t use memoization if the performance gain is negligible compared to the overhead of managing the cache.

Description: You are climbing a staircase. It takes n steps to reach the top. Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?

High-Level Approach using Memoization:

Define a recursive function ways_to_climb(n) that calculates the number of ways to climb n steps. Use a cache (dictionary or hash map) to store the results for previously computed values of n. The base cases are ways_to_climb(0) = 1 (one way to climb zero steps) and ways_to_climb(1) = 1 (one way to climb one step). For n > 1, the number of ways is the sum of ways_to_climb(n-1) and ways_to_climb(n-2). Before making recursive calls, check the cache; if a result exists, return it directly; otherwise, compute it, store it in the cache, and return it. This avoids redundant calculations.