DP vs Recursion vs Greedy Choosing the Right Tool in DSA Rust - Complexity Comparison
We want to understand how different problem-solving methods affect the time it takes to run code.
Which method grows faster or slower as the input gets bigger?
Analyze the time complexity of these three approaches solving the same problem.
// Recursive approach (exponential)
fn fib_recursive(n: usize) -> usize {
if n <= 1 { return n; }
fib_recursive(n - 1) + fib_recursive(n - 2)
}
// Dynamic Programming approach (linear)
fn fib_dp(n: usize) -> usize {
let mut dp = vec![0; n + 1];
dp[0] = 0;
dp[1] = 1;
for i in 2..=n {
dp[i] = dp[i - 1] + dp[i - 2];
}
dp[n]
}
// Greedy approach example (linear)
fn coin_change_greedy(coins: &[usize], amount: usize) -> usize {
let mut count = 0;
let mut remaining = amount;
for &coin in coins.iter().rev() {
count += remaining / coin;
remaining %= coin;
}
count
}
These snippets show recursion, dynamic programming, and greedy methods solving problems.
- Recursive: Calls itself twice for each number, repeating many calculations.
- Dynamic Programming: Uses a loop to fill a list once, no repeats.
- Greedy: Loops through coins once, simple repeated division and remainder.
- Dominant operation: Recursive calls in recursion, loops in DP and greedy.
See how the number of steps grows as input size increases.
| Input Size (n) | Recursive Calls / Loops |
|---|---|
| 10 | Recursive: ~177 calls, DP: 10 loops, Greedy: depends on coins count |
| 100 | Recursive: huge (millions), DP: 100 loops, Greedy: still depends on coins count |
| 1000 | Recursive: impractical, DP: 1000 loops, Greedy: same as before |
Recursive grows very fast, DP and Greedy grow slowly and predictably.
Time Complexity: O(2^n) for recursion, O(n) for DP, O(m) for greedy
Recursion can be very slow, DP and greedy are faster and scale better.
[X] Wrong: "Recursion is always slow and should be avoided."
[OK] Correct: Recursion is simple and good for small inputs or when combined with DP to avoid repeats.
Knowing when to use recursion, DP, or greedy shows you understand problem patterns and efficiency.
"What if we add memoization to the recursive approach? How would that change the time complexity?"
Practice
Solution
Step 1: Understand problem characteristics
Overlapping subproblems means the same smaller problems repeat. Optimal substructure means the best solution can be built from best solutions of smaller problems.Step 2: Match approach to characteristics
Dynamic Programming stores results of subproblems to avoid recomputation and uses optimal substructure to build the solution efficiently.Final Answer:
Dynamic Programming -> Option AQuick Check:
Overlapping subproblems + optimal substructure = Dynamic Programming [OK]
- Choosing greedy when subproblems overlap
- Using simple recursion without memoization causing slow code
- Confusing optimal substructure with greedy choice property
Solution
Step 1: Check function return type and recursion
fn factorial(n: u32) -> u32 { if n == 0 { 1 } else { n * factorial(n - 1) } } returns u32 and calls itself recursively correctly. fn factorial(n: u32) { if n == 0 { 1 } else { n * factorial(n - 1) } } lacks return type, so it returns unit ().Step 2: Validate correctness of recursion and return
fn factorial(n: u32) -> u32 { if n == 0 { 1 } else { n * factorial(n - 1) } } correctly returns the factorial value recursively. fn factorial(n: u32) -> u32 { let mut result = 1; for i in 1..=n { result *= i; } result } is iterative, not recursive. fn factorial(n: u32) -> String { if n == 0 { "1".to_string() } else { (n * factorial(n - 1)).to_string() } } returns String, not integer.Final Answer:
fn factorial(n: u32) -> u32 { if n == 0 { 1 } else { n * factorial(n - 1) } } -> Option DQuick Check:
Recursive function with correct return type = fn factorial(n: u32) -> u32 { if n == 0 { 1 } else { n * factorial(n - 1) } } [OK]
- Omitting return type causes compilation errors
- Confusing iterative and recursive implementations
- Returning wrong data types in recursion
fn fib(n: u32) -> u32 {
if n <= 1 {
n
} else {
fib(n - 1) + fib(n - 2)
}
}
fn main() {
println!("{}", fib(5));
}Solution
Step 1: Understand Fibonacci recursion
fib(5) = fib(4) + fib(3). Calculate fib(4) and fib(3) similarly until base cases fib(1)=1 and fib(0)=0.Step 2: Calculate fib(5) step-by-step
fib(0)=0, fib(1)=1, fib(2)=1, fib(3)=2, fib(4)=3, fib(5)=5.Final Answer:
5 -> Option BQuick Check:
Fibonacci of 5 = 5 [OK]
- Confusing fib(5) with fib(6)
- Miscounting base cases
- Assuming fib(0) = 1 instead of 0
fn coin_change(coins: &[u32], amount: u32) -> u32 {
let mut remaining = amount;
let mut count = 0;
for &coin in coins {
count += remaining / coin;
remaining %= coin;
}
if remaining == 0 { count } else { 0 }
}
fn main() {
let coins = [1, 2, 5];
println!("{}", coin_change(&coins, 11));
}Solution
Step 1: Check coin order
Greedy coin change requires coins sorted in descending order to pick largest first. Here coins=[1, 2, 5] are ascending, so it picks smallest first.Step 2: Trace execution
For amount=11: coin=1, count=11, remaining=0; subsequent coins add 0. Returns 11, but optimal is 3 (5+5+1). Greedy suboptimal.Final Answer:
Coins are not sorted in descending order, so greedy fails -> Option CQuick Check:
Greedy needs descending coin order [OK]
- Assuming greedy always works for any coin set
- Ignoring coin order importance
- Confusing return values for impossible change
Solution
Step 1: Analyze greedy suitability
Greedy fails here because picking largest coin first (4) may not lead to minimum coins for all amounts (e.g., amount=6).Step 2: Consider recursion and DP
Recursion without memoization repeats calculations and is inefficient. Dynamic programming stores results for smaller amounts, avoiding recomputation and ensuring minimum coins.Final Answer:
Use dynamic programming to store minimum coins needed for smaller amounts and build up -> Option AQuick Check:
Minimum coins with overlapping subproblems = Dynamic Programming [OK]
- Assuming greedy always works for coin change
- Using recursion without memoization causing slow code
- Ignoring overlapping subproblems in coin change
Ready to prove it?
Pass the test and this pattern moves unknown
