Memoization to Optimize Recursion in DSA Rust - Time & Space Complexity
When using recursion, some problems repeat the same work many times. Memoization helps by remembering results to avoid repeated work.
We want to see how this remembering changes the time needed as input grows.
Analyze the time complexity of the following code snippet.
fn fib_memo(n: usize, memo: &mut Vec<Option<usize>>) -> usize {
if n <= 1 {
return n;
}
if let Some(value) = memo[n] {
return value;
}
let result = fib_memo(n - 1, memo) + fib_memo(n - 2, memo);
memo[n] = Some(result);
result
}
This code calculates the nth Fibonacci number using recursion with memoization to store results.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Recursive calls to fib_memo for smaller n.
- How many times: Each unique n from 0 to n is computed once and then reused.
Each Fibonacci number from 0 up to n is calculated once and stored. So the work grows roughly in a straight line with n.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 calculations |
| 100 | About 100 calculations |
| 1000 | About 1000 calculations |
Pattern observation: The number of operations grows directly with n, not exponentially as in plain recursion.
Time Complexity: O(n)
This means the time needed grows in a straight line as the input number n grows.
[X] Wrong: "Memoization makes recursion instantly fast for all problems."
[OK] Correct: Memoization only helps if the problem has overlapping subproblems. If each call is unique, memoization won't reduce work.
Understanding how memoization changes recursion time is a key skill. It shows you can improve slow solutions by remembering past work, a common real-world need.
"What if we removed memoization and just used plain recursion? How would the time complexity change?"
Practice
memoization in recursive functions?Solution
Step 1: Understand what memoization does
Memoization saves the results of function calls so that if the same inputs occur again, the saved result is used instead of recalculating.Step 2: Identify the benefit in recursion
This avoids repeated calculations in recursive calls, making the function faster and more efficient.Final Answer:
To store results of expensive function calls and reuse them -> Option AQuick Check:
Memoization = Store and reuse results [OK]
- Thinking memoization slows down the function
- Confusing memoization with avoiding recursion
- Believing memoization always increases memory without benefit
HashMap for a recursive function?Solution
Step 1: Check correct HashMap initialization syntax
In Rust, to create a mutable HashMap, you uselet mut memo: HashMap<u32, u32> = HashMap::new();.Step 2: Identify incorrect options
Vec::new()uses the wrong type.let memo = HashMap<u32, u32>;missesmutandHashMap::new().let memo: HashMap = new HashMap();has invalid syntax.Final Answer:
let mut memo: HashMap<u32, u32> = HashMap::new(); -> Option DQuick Check:
Correct HashMap init = let mut memo: HashMap<u32, u32> = HashMap::new(); [OK]
- Forgetting to make the HashMap mutable
- Using Vec instead of HashMap for memoization
- Missing the ::new() method call
use std::collections::HashMap;
fn fib(n: u32, memo: &mut HashMap) -> u32 {
if n <= 1 {
return n;
}
if let Some(&val) = memo.get(&n) {
return val;
}
let result = fib(n - 1, memo) + fib(n - 2, memo);
memo.insert(n, result);
result
}
fn main() {
let mut memo = HashMap::new();
println!("{}", fib(5, &mut memo));
}Solution
Step 1: Understand the Fibonacci function with memoization
The function returns the nth Fibonacci number. fib(0)=0, fib(1)=1, fib(5) = fib(4)+fib(3).Step 2: Calculate fib(5)
fib(2)=1, fib(3)=2, fib(4)=3, fib(5)=5. The memo cache stores intermediate results to avoid repeated calls.Final Answer:
5 -> Option BQuick Check:
fib(5) = 5 [OK]
- Confusing fib(5) with fib(6)
- Ignoring memoization effect on performance
- Misreading base cases as 1 instead of 0
use std::collections::HashMap;
fn factorial(n: u32, memo: &mut HashMap) -> u32 {
if n == 0 {
return 1;
}
if memo.contains_key(&n) {
return *memo[&n];
}
let result = n * factorial(n - 1, memo);
memo.insert(n, result);
result
}
fn main() {
let mut memo = HashMap::new();
println!("{}", factorial(5, &mut memo));
}Solution
Step 1: Check how memo is accessed
Usingmemo[&n]directly can panic if the key is missing. Better to usememo.get(&n)or unwrap safely.Step 2: Verify base case and memo usage
Base case n==0 is correct for factorial. Memo is passed mutably correctly. Memoization is used properly except for unsafe access.Final Answer:
Using memo[&n] causes a panic if key is missing -> Option AQuick Check:
Unsafe HashMap access = Using memo[&n] causes a panic if key is missing [OK]
- Assuming memo[&n] never panics
- Changing correct base case unnecessarily
- Forgetting to pass memo mutably
n stairs, taking 1 or 2 steps at a time. Which memoization approach below correctly implements this in Rust?Solution
Step 1: Understand the problem and memoization need
Calculating ways to climb stairs with 1 or 2 steps is a classic recursion problem that benefits from memoization to avoid repeated calculations.Step 2: Identify correct memoization method
Using a mutable HashMap keyed by stair number to store and reuse results before recursive calls is the correct approach. Other options either do not store results or do not initialize storage properly.Final Answer:
Use a mutable HashMap to store results keyed by stair number and check before recursive calls -> Option CQuick Check:
Memoize with HashMap keyed by input = Use a mutable HashMap to store results keyed by stair number and check before recursive calls [OK]
- Not storing intermediate results
- Using global counters instead of memo
- Using uninitialized vectors for memo
Ready to prove it?
Pass the test and this pattern moves unknown
