Home DSA Fibonacci with Dynamic Programming Explained — Memoization vs Tabulation

Fibonacci with Dynamic Programming Explained — Memoization vs Tabulation

In Plain English 🔥
Imagine you're asked the same maths question 50 times in a row — you'd write the answer on a sticky note after the first time and just read it instead of re-calculating. That sticky note trick is exactly what Dynamic Programming does for Fibonacci. The naive approach recalculates fib(3) dozens of times; DP writes down each answer the moment it's found and reuses it instantly. It turns a slow, forgetful algorithm into a fast, remembering one.
⚡ Quick Answer
Imagine you're asked the same maths question 50 times in a row — you'd write the answer on a sticky note after the first time and just read it instead of re-calculating. That sticky note trick is exactly what Dynamic Programming does for Fibonacci. The naive approach recalculates fib(3) dozens of times; DP writes down each answer the moment it's found and reuses it instantly. It turns a slow, forgetful algorithm into a fast, remembering one.

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).

NaiveFibonacci.java · JAVA
1234567891011121314151617181920212223242526272829
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).");
    }
}
▶ Output
fib(5) = 5
fib(10) = 55
fib(20) = 6765

fib(40) = 102334155

Notice fib(40) was slow. Imagine fib(80).
⚠️
Watch Out:fib(50) with naive recursion takes several minutes on modern hardware. If you're live-coding in an interview and you write this version, say immediately: 'This is O(2^n) — let me optimise it with memoization.' Showing self-awareness is half the battle.

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.

MemoizedFibonacci.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
import java.util.Arrays;

public class MemoizedFibonacci {

    /**
     * Top-Down Dynamic ProgrammingMemoization.
     * 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.");
    }
}
▶ Output
=== Memoized Fibonacci ===
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.
⚠️
Pro Tip:Initialise your memo array with -1, not 0. fib(0) is legitimately 0, so using 0 as your 'not computed' sentinel will make your code incorrectly think fib(0) is already cached — and return wrong answers. Always pick a sentinel value that can never be a real answer.

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.

TabulatedFibonacci.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
public class TabulatedFibonacci {

    /**
     * Bottom-Up Dynamic ProgrammingTabulation.
     * 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.");
    }
}
▶ Output
=== Tabulated Fibonacci (Full Table) ===
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.
🔥
Interview Gold:When an interviewer asks you to 'optimise the space complexity', they're fishing for the two-variable sliding window trick. Show fibSpaceOptimised() and explain that since fib(n) only depends on fib(n-1) and fib(n-2), you never need more than two variables in memory. This answer consistently impresses.
Feature / AspectMemoization (Top-Down)Tabulation (Bottom-Up)
DirectionStarts at fib(n), recurses downStarts at fib(0), loops up
Uses Recursion?Yes — recursive calls with a cacheNo — pure iterative loop
Risk of Stack Overflow?Yes — for very large n (n > ~10,000)No — no call stack used
Time ComplexityO(n) — each value computed onceO(n) — single forward pass
Space ComplexityO(n) — memo array + call stackO(n) table, or O(1) optimised
Ease to WriteEasy — small change from naive recursionSlightly more deliberate setup
Best Use CaseWhen you don't need all sub-problemsWhen you need all values or max speed
Solves Only Needed Sub-problems?Yes — lazy evaluationNo — 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.

🔥
TheCodeForge Editorial Team Verified Author

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.

← PreviousRod Cutting ProblemNext →Subset Sum Problem
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged