DP vs Recursion vs Greedy Choosing the Right Tool in DSA Go - Complexity Comparison
Choosing between dynamic programming, recursion, and greedy methods affects how fast your solution runs.
We want to see how the time needed changes with input size for each approach.
Analyze the time complexity of these three approaches solving the same problem.
// Recursive Fibonacci (simple recursion)
func fibRec(n int) int {
if n <= 1 {
return n
}
return fibRec(n-1) + fibRec(n-2)
}
// Dynamic Programming Fibonacci (memoization)
func fibDP(n int, memo map[int]int) int {
if n <= 1 {
return n
}
if val, ok := memo[n]; ok {
return val
}
memo[n] = fibDP(n-1, memo) + fibDP(n-2, memo)
return memo[n]
}
// Greedy approach example (not suitable for Fibonacci but for illustration)
func fibGreedy(n int) int {
a, b := 0, 1
for i := 0; i < n; i++ {
a, b = b, a+b
}
return a
}
These functions calculate Fibonacci numbers using recursion, dynamic programming, and a greedy-like iterative method.
Look at loops and repeated calls that cost time.
- Recursive: Calls itself twice per call, many repeated calculations.
- Dynamic Programming: Recursion with memo saves results, each input computed once.
- Greedy (Iterative): Single loop runs n times, no repeated work.
- Dominant operation: Number of function calls or loop iterations.
See how the number of steps grows as n gets bigger.
| Input Size (n) | Recursive Calls / Iterations |
|---|---|
| 10 | Recursive: ~177 calls, DP: 19 calls, Greedy: 10 iterations |
| 100 | Recursive: huge (exponential), DP: 199 calls, Greedy: 100 iterations |
| 1000 | Recursive: impractical, DP: 1999 calls, Greedy: 1000 iterations |
Recursive grows very fast, DP and Greedy grow linearly with input size.
Time Complexity: O(2^n) for Recursion, O(n) for DP and Greedy
Simple recursion repeats work exponentially, while DP and Greedy do work once per input.
[X] Wrong: "Recursion is always slow and should be avoided."
[OK] Correct: Recursion can be efficient with memoization (DP), and sometimes simpler to write.
Understanding these differences helps you pick the right tool and explain your choice clearly in interviews.
"What if we replaced memoization in DP with bottom-up tabulation? How would the time complexity change?"
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 parts.Step 2: Match approach to characteristics
Dynamic Programming stores results of subproblems to avoid repeated work, ideal for overlapping subproblems and optimal substructure.Final Answer:
Dynamic Programming -> Option AQuick Check:
Overlapping subproblems + optimal substructure = Dynamic Programming [OK]
- Choosing greedy when subproblems overlap
- Using simple recursion without memoization
- Confusing brute force with DP
Solution
Step 1: Check function return type and base case
Factorial returns int, so function must return int. Base case for factorial is n == 0 returning 1.Step 2: Verify recursive call and return statement
Recursive call multiplies n by factorial(n-1). func factorial(n int) int { if n == 0 { return 1 } return n * factorial(n-1) } matches correct signature and logic.Final Answer:
func factorial(n int) int { if n == 0 { return 1 } return n * factorial(n-1) } -> Option BQuick Check:
Correct return type and base case = func factorial(n int) int { if n == 0 { return 1 } return n * factorial(n-1) } [OK]
- Missing return type in function signature
- Incorrect base case value
- Returning wrong type or no return
func fib(n int) int {
if n <= 1 {
return n
}
return fib(n-1) + fib(n-2)
}
func main() {
fmt.Println(fib(5))
}Solution
Step 1: Trace fib(5) calls
fib(5) = fib(4) + fib(3); fib(4) = fib(3) + fib(2); fib(3) = fib(2) + fib(1); fib(2) = fib(1) + fib(0)Step 2: Calculate values bottom-up
fib(0)=0, fib(1)=1; fib(2)=1; fib(3)=2; fib(4)=3; fib(5)=5Final Answer:
5 -> Option CQuick Check:
Fibonacci(5) = 5 [OK]
- Confusing index starting point
- Miscomputing recursive calls
- Assuming fib(1) = 0
func coinChange(coins []int, amount int) int {
count := 0
for i := len(coins) - 1; i >= 0; i-- {
for amount >= coins[i] {
amount -= coins[i]
count++
}
}
if amount != 0 {
return -1
}
return count
}Solution
Step 1: Understand greedy coin change logic
The code tries largest coins first, subtracting from amount until no longer possible.Step 2: Identify limitation of greedy approach
Greedy works only if coin denominations are canonical (like US coins). Otherwise, it may not find minimum coins or any solution.Final Answer:
Greedy may fail if coins are not canonical denominations -> Option AQuick Check:
Greedy correctness depends on coin set [OK]
- Assuming greedy always finds minimum coins
- Ignoring coin order importance
- Misunderstanding return values
Solution
Step 1: Analyze LIS problem nature
LIS requires checking subsequences and their lengths. Simple recursion tries all but is exponential time.Step 2: Compare approaches
Greedy fails because local choices don't guarantee global LIS. DP stores LIS lengths for subproblems, improving efficiency.Final Answer:
Dynamic Programming is best for LIS -> Option DQuick Check:
LIS problem = DP best approach [OK]
- Choosing greedy for LIS
- Ignoring exponential time of recursion
- Thinking brute force is efficient
Ready to prove it?
Pass the test and this pattern moves unknown
