Fibonacci with Dynamic Programming Explained — Memoization vs Tabulation
Fibonacci numbers look deceptively simple — 0, 1, 1, 2, 3, 5, 8, 13… — but they hide one of the most important lessons in computer science: redundant computation is the silent killer of performance. Interviewers use Fibonacci precisely because it's a lens that reveals whether you understand why brute-force recursion fails at scale. Google, Meta, and Amazon engineers routinely ask candidates to optimise it, because the thinking you apply here transfers directly to harder DP problems like longest common subsequence, coin change, and edit distance.
The core problem is this: computing the 50th Fibonacci number with plain recursion makes over 2 billion function calls. That's not an exaggeration — the call tree branches like a fracklace, recalculating the same sub-results exponentially. Dynamic Programming (DP) fixes this by storing results the first time they're computed so they're never calculated twice. The difference between O(2^n) and O(n) is the difference between your program taking years versus milliseconds.
By the end of this article you'll be able to explain — out loud, to an interviewer — why naive recursion is slow, implement both DP approaches (memoization and tabulation) from scratch in Java, compare their trade-offs, and avoid the three mistakes that trip up most beginners. No prior DP knowledge is assumed. We'll build everything piece by piece.
Why Naive Recursion Destroys Performance — The Problem We're Solving
Before we fix anything, we need to feel the pain of the broken version. The recursive Fibonacci definition is beautiful on paper: fib(n) = fib(n-1) + fib(n-2). But when you translate that directly into code, something ugly happens under the hood.
Every call to fib(n) spawns two more calls. Those each spawn two more. By the time you want fib(50), the call tree has over two billion nodes — and most of them are duplicates. fib(48) is calculated twice, fib(47) three times, fib(46) five times. The overlap grows exponentially.
Think of it like baking a birthday cake by going to the shop to buy flour every single time you need a cup of it — including mid-bake. You'd make fifty trips. The sensible thing is to buy all the flour at the start and measure from the bag. DP is that bag of flour.
The time complexity of naive recursion is O(2^n). At n=50, that's roughly 1,125,899,906,842,624 operations. Modern laptops do about 10^9 operations per second — so you'd be waiting over two weeks. This is not theoretical; run the code below and watch your terminal freeze at fib(50).
public class NaiveFibonacci { /** * Naive recursive Fibonacci — correct but catastrophically slow. * Time complexity: O(2^n) — doubles the work for every extra step. * Space complexity: O(n) — call stack depth equals n. */ public static long fib(int n) { // Base cases: fib(0) = 0, fib(1) = 1 — the sequence's starting seeds if (n == 0) return 0; if (n == 1) return 1; // Every call spawns TWO more calls — this is where the explosion happens return fib(n - 1) + fib(n - 2); } public static void main(String[] args) { // These small values are fine — the tree is tiny System.out.println("fib(5) = " + fib(5)); System.out.println("fib(10) = " + fib(10)); System.out.println("fib(20) = " + fib(20)); // Count how many times fib(3) is recalculated inside fib(10) // (It's called 8 times — wasteful even at n=10) System.out.println("\nfib(40) = " + fib(40)); // starts to feel slow // DO NOT try fib(60) here — it will run for minutes System.out.println("\nNotice fib(40) was slow. Imagine fib(80)."); } }
fib(10) = 55
fib(20) = 6765
fib(40) = 102334155
Notice fib(40) was slow. Imagine fib(80).
Memoization — The Top-Down DP Approach (With a Notebook Analogy)
Memoization is the first way to apply DP. The word comes from 'memo' — as in, you write a memo to yourself so you don't repeat work. The strategy is top-down: start at the big problem (fib(n)), break it down recursively as before, but this time write down every answer in a notebook (an array or HashMap) the first time you compute it. Next time the same question comes up, just read from the notebook.
Here's the analogy in full: imagine you're a student and your teacher keeps asking you random Fibonacci questions during class. The first time they ask 'what's fib(10)?', you work it out and write '55' next to 'fib(10)' in your notebook. The next time they ask fib(10) — even mid-calculation of fib(12) — you just glance at your notebook. Zero thinking required.
The code change is minimal but the impact is massive. We add a memo array initialised to -1. Before computing, we check: 'have I seen this before?' If yes, return the stored answer. If no, compute, store, then return.
Time complexity drops from O(2^n) to O(n). Space complexity is O(n) for the memo table plus O(n) for the call stack — so O(n) overall. Every unique sub-problem is solved exactly once.
import java.util.Arrays; public class MemoizedFibonacci { /** * Top-Down Dynamic Programming — Memoization. * We use a memo array as our 'notebook'. * Time: O(n) — each unique fib value computed exactly once. * Space: O(n) — memo array + recursive call stack. */ public static long fib(int n, long[] memo) { // Base cases — the two seeds every Fibonacci sequence starts from if (n == 0) return 0; if (n == 1) return 1; // CHECK THE NOTEBOOK FIRST — did we already compute this? // memo[n] != -1 means we've been here before and stored the answer if (memo[n] != -1) { return memo[n]; // Return instantly — zero recursion needed } // First time visiting fib(n) — compute it the normal recursive way... long result = fib(n - 1, memo) + fib(n - 2, memo); // ...then WRITE IT IN THE NOTEBOOK before returning memo[n] = result; return result; } public static void main(String[] args) { int target = 50; // Create the memo array, sized (target + 1) so index n maps to fib(n) // Fill with -1 to mean 'not yet computed' long[] memo = new long[target + 1]; Arrays.fill(memo, -1); System.out.println("=== Memoized Fibonacci ==="); // These will all be instant — even fib(50) completes in microseconds for (int i = 0; i <= 10; i++) { // Reuse the same memo table — values computed earlier help later calls System.out.printf("fib(%2d) = %d%n", i, fib(i, memo)); } System.out.println("..."); System.out.printf("fib(50) = %d%n", fib(target, memo)); System.out.println("\nCompleted instantly. Compare that to naive recursion."); } }
fib( 0) = 0
fib( 1) = 1
fib( 2) = 1
fib( 3) = 2
fib( 4) = 3
fib( 5) = 5
fib( 6) = 8
fib( 7) = 13
fib( 8) = 21
fib( 9) = 34
fib(10) = 55
...
fib(50) = 12586269025
Completed instantly. Compare that to naive recursion.
Tabulation — The Bottom-Up DP Approach (No Recursion at All)
Memoization is top-down: start big, recurse down. Tabulation is the opposite — bottom-up: start from the smallest known answers and build upward deliberately, filling a table row by row.
Think of it like filling in a multiplication table at school. You don't start at the 12×12 corner and work backwards — you start at 1×1 and fill forward because each cell depends only on cells you've already filled.
For Fibonacci: fib(0) = 0 and fib(1) = 1 are our starting seeds. From there, every subsequent value is just the sum of the previous two entries in our table. We never recurse. No call stack. Just a simple loop.
This is generally preferred in production code for three reasons: no risk of stack overflow for large n, slightly faster in practice due to no function call overhead, and easier to reason about memory usage. The space can even be reduced to O(1) by keeping only the last two values — we'll show that optimisation too.
Time complexity: O(n). Space complexity: O(n) for the full table, or O(1) with the optimised two-variable version.
public class TabulatedFibonacci { /** * Bottom-Up Dynamic Programming — Tabulation. * Build the answer from the ground up using a table. * Time: O(n) — single pass through the loop. * Space: O(n) — the fibTable array stores all intermediate results. */ public static long fibTableFull(int n) { if (n == 0) return 0; if (n == 1) return 1; // Create a table where index i will hold the value of fib(i) long[] fibTable = new long[n + 1]; // Seed the table with the two values we know for certain fibTable[0] = 0; // fib(0) is defined as 0 fibTable[1] = 1; // fib(1) is defined as 1 // Fill every cell from index 2 upward — each depends only on earlier cells for (int position = 2; position <= n; position++) { // The core DP recurrence: sum the two cells immediately before this one fibTable[position] = fibTable[position - 1] + fibTable[position - 2]; } // The answer we want is sitting at the end of the table return fibTable[n]; } /** * Space-Optimised Tabulation — O(1) space. * We only ever need the PREVIOUS TWO values, so we ditch the full table. * This is the version you want in memory-constrained environments. * Time: O(n) — same single loop. * Space: O(1) — just three variables, no array. */ public static long fibSpaceOptimised(int n) { if (n == 0) return 0; if (n == 1) return 1; long previousPrevious = 0; // Represents fib(i-2) long previous = 1; // Represents fib(i-1) long current = 0; // Will hold fib(i) each iteration for (int step = 2; step <= n; step++) { current = previous + previousPrevious; // Compute this step's fib value previousPrevious = previous; // Slide the window forward previous = current; // Slide the window forward } return current; } public static void main(String[] args) { System.out.println("=== Tabulated Fibonacci (Full Table) ==="); for (int i = 0; i <= 10; i++) { System.out.printf("fib(%2d) = %d%n", i, fibTableFull(i)); } System.out.printf("fib(50) = %d%n%n", fibTableFull(50)); System.out.println("=== Space-Optimised Fibonacci (O(1) space) ==="); System.out.printf("fib(50) = %d%n", fibSpaceOptimised(50)); System.out.printf("fib(70) = %d%n", fibSpaceOptimised(70)); System.out.println("\nSame answers, no array needed — just three variables."); } }
fib( 0) = 0
fib( 1) = 1
fib( 2) = 1
fib( 3) = 2
fib( 4) = 3
fib( 5) = 5
fib( 6) = 8
fib( 7) = 13
fib( 8) = 21
fib( 9) = 34
fib(10) = 55
fib(50) = 12586269025
=== Space-Optimised Fibonacci (O(1) space) ===
fib(50) = 12586269025
fib(70) = 190392490709135
Same answers, no array needed — just three variables.
| Feature / Aspect | Memoization (Top-Down) | Tabulation (Bottom-Up) |
|---|---|---|
| Direction | Starts at fib(n), recurses down | Starts at fib(0), loops up |
| Uses Recursion? | Yes — recursive calls with a cache | No — pure iterative loop |
| Risk of Stack Overflow? | Yes — for very large n (n > ~10,000) | No — no call stack used |
| Time Complexity | O(n) — each value computed once | O(n) — single forward pass |
| Space Complexity | O(n) — memo array + call stack | O(n) table, or O(1) optimised |
| Ease to Write | Easy — small change from naive recursion | Slightly more deliberate setup |
| Best Use Case | When you don't need all sub-problems | When you need all values or max speed |
| Solves Only Needed Sub-problems? | Yes — lazy evaluation | No — computes all values up to n |
🎯 Key Takeaways
- Naive recursion is O(2^n) because it recomputes the same sub-problems exponentially — fib(3) alone is called 8 times inside fib(10).
- Memoization (top-down) fixes this by caching results in an array with a -1 sentinel — cutting the time to O(n) with a minimal code change.
- Tabulation (bottom-up) avoids recursion entirely by filling a table from index 0 upward — no stack overflow risk, and space can be reduced to O(1) with two variables.
- Fibonacci is a gateway problem — the exact same DP thinking (identify overlapping sub-problems, store results, reuse them) scales directly to coin change, knapsack, and edit distance.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Using 0 as the memo sentinel value — Since fib(0) is legitimately 0, initialising your memo array with zeros makes your code think fib(0) is already cached on every call, returning 0 for values that should be different. Fix: always initialise with -1 (Arrays.fill(memo, -1L)) so that -1 unambiguously means 'not yet computed'.
- ✕Mistake 2: Using int instead of long for Fibonacci values — fib(46) = 1,836,311,903 which still fits in an int, but fib(47) = 2,971,215,073 which overflows silently, giving you a negative number with no error thrown. Fix: always use long for Fibonacci (or BigInteger for n > 92). The symptom is unexpected negative results for n >= 47.
- ✕Mistake 3: Creating a new memo array inside the recursive method — If you declare long[] memo = new long[n+1] inside the fib() method body, every recursive call creates a fresh empty array, so no caching happens at all and you're back to O(2^n) performance. Fix: initialise the memo array once in main() and pass it as a parameter into the recursive method, exactly as shown in MemoizedFibonacci.java above.
Interview Questions on This Topic
- QWhat is the time complexity of naive recursive Fibonacci and why — can you draw the call tree for fib(5) to prove it?
- QWhat's the difference between memoization and tabulation? Which would you choose and why — give a concrete scenario where one beats the other.
- QIf I asked you to compute fib(1,000,000), what breaks in both DP approaches and how would you fix each issue? (Hint: think stack overflow for memoization and integer overflow for both.)
Frequently Asked Questions
What is the difference between memoization and dynamic programming?
Memoization is one technique within dynamic programming. DP is the broader strategy of solving problems by breaking them into overlapping sub-problems and storing results to avoid recomputation. Memoization achieves this top-down using recursion plus a cache; tabulation achieves it bottom-up using an iterative loop and a table. Both are valid DP approaches.
Why does Fibonacci with plain recursion get so slow so fast?
Because every call to fib(n) makes two more calls, the number of total calls grows as 2^n — doubling for every single step you add. At n=50 that's over a quadrillion operations. The root cause is that identical sub-problems like fib(10) are recomputed from scratch every time they appear in the tree instead of being looked up from a cache.
When should I use memoization over tabulation in an interview?
Use memoization when the solution feels naturally recursive (you think top-down), when you only need some sub-problems computed (sparse access patterns), or when the interviewer wants to see recursive thinking. Use tabulation when they ask about space efficiency, when n could be very large (stack overflow risk), or when you need to print all Fibonacci values up to n. In practice, tabulation is safer for production code.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.