Saturday, June 28, 2025

A much-improved Fibonacci approach

 In the last article, we looked in depth at the enormous cost of even fib(7) by drawing out the tree of calls for it and realizing that going even to fib(8) would be much more expensive. The traditional recursive way, we concluded, was beautiful and simple, but terribly inefficient due to its exponential runtime.

Of course, almost anything is better than an exponential runtime, so we’ll take any improvement we can get.  But here’s the good news: I claim (and will soon prove) that I have a method that takes O(2^n) down to O(n). Those are enormous savings: in the first case, if n is 10, you do 1024 operations, whereas in the second case, with the same n, you only do 10—saving >=99% of the work from the exponential version, getting the answer that much more quickly and efficiently.

Here's how we can do it:

  • Set up an array
  • Use the array to remember values you’ve already calculated
  • Look them up, instead of recalculating them
  • Keep adding values as you calculate new ones


For comparison’s sake, let’s go calculate fib(7). For this, we need an array storing 8 integers, so we can index them from 0 to 7.

Initially, of course, the array looks like

Index

0

1

2

3

4

5

6

7

Value

0

0

0

0

0

0

0

0


But we can start by populating the values of the base cases, given in the recursive formula, namely, that fib(0)—or, here, in this context, fib[0] with square brackets because we’re accessing an index of an array, not calling a function—is 1, and so is fib[1].

Index

0

1

2

3

4

5

6

7

Value

1

1

0

0

0

0

0

0

 

Now, from fib[2] onward, let’s simply loop through the array and do one of two things:

  • Use an existing value
  • Update a 0 into a real value, where necessary

fib[2] is not a base case, but we know the rule is to add the previous two, so this is as simple as fib[2] = fib[0] + fib[1];

Index

0

1

2

3

4

5

6

7

Value

1

1

2

0

0

0

0

0

Now, we have fib[2] saved somewhere, and we’ll save ourselves a lot of time by not throwing that result away.

We do the same for fib[3], which is fib[2] + fib[1]. fib[1] was a base case, so we hard-coded that earlier, and we just calculated and saved fib[2], so we can use it

Index

0

1

2

3

4

5

6

7

Value

1

1

2

3

0

0

0

0

 
Likewise, to get fib[4] by looking up (rather than recalculating) fib[2] and fib[3]:

Index

0

1

2

3

4

5

6

7

Value

1

1

2

3

5

0

0

0

 
Likewise, to get fib[5] by looking up (rather than recalculating) fib[3] and fib[4]:

Index

0

1

2

3

4

5

6

7

Value

1

1

2

3

5

8

0

0

 

Likewise, to get fib[6] by looking up (rather than recalculating) fib[4] and fib[5]:

Index

0

1

2

3

4

5

6

7

Value

1

1

2

3

5

8

13

0

 

Likewise, to get fib[7]—our final answer— by looking up (rather than recalculating) fib[5] and fib[6]:

Index

0

1

2

3

4

5

6

7

Value

1

1

2

3

5

8

13

21

This technique of storing a value and using it later is called memoizing (for as much trouble as my word processor gives me, there is no “r” as in “memoRizing”—it’s “memoizing” as in “writing a memo”) and is key to a pattern of programming called dynamic programming, which we’ll start covering explicitly soon.

This way, you still have the relationship that f(n) = f(n-1) + f(n-2), but, thanks to the fact that you’re storing these values somewhere, you don’t need to waste any time redoing past calculations. 


Using a HashMap, we can write this much-improved solution like this:

import java.util.HashMap;

import java.util.Map;


public class FibonacciMemoization {

    // Cache to store already computed Fibonacci values

    private static Map<Integer, Long> memo = new HashMap<>();


    public static long fibonacci(int n) {

        // Base cases

        if (n == 0) return 0;

        if (n == 1) return 1;


        // Check if already computed

        if (memo.containsKey(n)) {

            return memo.get(n);

        }


        // Compute, store, and return

        long result = fibonacci(n - 1) + fibonacci(n - 2);

        memo.put(n, result);

        return result;

    }

}


No comments:

Post a Comment

Switch

 Other than if/if-else/if-else if-else and the ternary operator, there is yet another common and important conditional expression in Java th...