Execution algorithm recursively seeking lower number, is very slow
Data Structures & Algorithms practice on Codemia
Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.
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
Related reading
- Expand a random range from 1–5 to 1–7
- Explain how recursion works in an algorithm to determine depth of binary tree?
- Explain markov-chain algorithm in layman's terms
- Explain the implementation of Euler's Totient Implementation
- Executors.newCachedThreadPool versus Executors.newFixedThreadPool
- Existing tools to find unused tables in cassandra cluster
- Explain this On log n algorithm for the Cat/Egg Throwing Problem
- Explain this snippet which finds the maximum of two integers without using if-else or any other comparison operator?

DSA Fundamentals
Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Data Structures & Algorithms practice on Codemia
Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.