Execution algorithm recursively seeking lower number, is very slow
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
A common performance problem in recursive algorithms is exponential time complexity caused by recomputing the same subproblems many times. When a recursive function seeks a lower number (like computing Fibonacci, counting change, or finding minimum steps), naive recursion explores an exponential number of paths because overlapping subproblems are solved repeatedly. The fix is memoization or converting to an iterative dynamic programming approach.
The Problem: Exponential Recursion
Consider a classic example — computing the nth Fibonacci number:
This looks clean but is extremely slow. fib(50) makes over 12 billion recursive calls because each call branches into two more:
The time complexity is O(2^n) because each call generates two more calls, and subproblems like fib(3) are recomputed exponentially many times.
Why Recursive Descent is Slow
The core issue is overlapping subproblems. The recursive function:
- Breaks the problem into smaller subproblems
- Solves the same subproblem multiple times from different call paths
- Has no memory of previous results
This pattern appears in many algorithms:
| Problem | Naive Recursion | With Optimization |
| Fibonacci | O(2^n) | O(n) |
| Coin change | O(S^n) | O(n * S) |
| Minimum steps | O(k^n) | O(n) |
| Edit distance | O(3^n) | O(m * n) |
| Knapsack | O(2^n) | O(n * W) |
Fix 1: Memoization (Top-Down DP)
Add a cache to store results of subproblems:
Or with a manual dictionary:
Each subproblem is computed only once, reducing time complexity from O(2^n) to O(n).
Fix 2: Iterative Dynamic Programming (Bottom-Up)
Build the solution iteratively from the base case up:
Benefits over memoization:
- No recursion stack overhead (no risk of stack overflow)
- O(1) space when only the last few values are needed
- Slightly faster due to no function call overhead
Real-World Example: Minimum Coin Change
Given coins [1, 5, 10, 25], find the minimum coins to make amount n:
How to Identify the Problem
Signs that your recursion has overlapping subproblems:
- The function calls itself multiple times with arguments that decrease toward a base case
- Different call paths reach the same arguments — draw the call tree and look for duplicates
- Runtime grows exponentially with input size —
n=20is fast,n=40takes minutes,n=50never finishes
Profiling
Fix 3: Tail Recursion (Language-Dependent)
Some languages optimize tail-recursive calls. Python does not, but here is the pattern:
In languages like Scheme, Haskell, or Scala, this compiles to an iterative loop automatically.
Common Pitfalls
- Mutable default arguments in Python: Using
memo={}as a default parameter shares the cache across calls, which is usually what you want but can cause issues if the function is called with different problem contexts. Use@lru_cacheinstead. - Stack overflow with deep recursion: Even with memoization, Python's default recursion limit is 1000. For large inputs, either increase it with
sys.setrecursionlimit()or use the iterative approach. - Not recognizing overlapping subproblems: Not every recursive algorithm benefits from memoization. If subproblems do not overlap (like merge sort), memoization adds overhead without benefit.
- Space complexity: Memoization caches all subproblem results. For problems where you only need the last few values (like Fibonacci), the iterative approach with O(1) space is better.
Summary
- Naive recursion that seeks lower numbers often has exponential O(2^n) or O(k^n) complexity due to overlapping subproblems
- Memoization (
@lru_cacheor dictionary) reduces this to polynomial time by caching results - Iterative DP (bottom-up) avoids recursion overhead and often uses less memory
- Draw the recursion tree to identify if subproblems are being recomputed
- If runtime doubles each time input grows by 1, you have an exponential recursion problem

