Analyzing nth Fibonacci Solutions

javascript recursion

Calculating the Fibonacci sequence is one of the first programs many people learn to write once they’re past Hello World. Of course, there are many ways of solving this problem; for example, it can be solved iteratively, or recursively. I was curious if my intuitive expectations about the performance of a few different methods were correct, so I wrote five different programs that all return the nth Fibonacci number and put them to the test.

I first tested each function by running it in node, passing in increasing values of n until a) it took so long to return I lost patience, or b) the max call size was reached. FYI, I used my 2013 Mac with an i7 processor running Yosemite.

Basic Recursion

First up, I tried out a basic recursive function. Notice how simple this functions is (only one line!)–it reads almost the same as the definition of the sequence: The next value in the sequence is found by summing the previous two values–except for when n = 0 and n = 1; these values are equal to n.

//Basic Recursive
var rFib = function(n){
  return n<2 ? n : rFib(n-1) + rFib(n-2);
};

Unfortunately, this simple program can only handle small values of n. When n = 45, it average 21.5s before returning. I ran out of patience trying to test it for values greater than 45. This makes sense, as this program has a time complexity of O(2n)–not good. Incidentally, it has a space complexity of O(n). Also, notice in the figure below how many redundant computations this methods uses! Blech. We can do better.

Recursion Tree

Iterative Recursion

Next up, let’s check out an iterative approach that uses recursion to iterate (this is quite a bit different than the approach above). This time, we’re using a recursive helper function that allows us to iterate n times, passing in the previous two values and decrementing n each time. We return the last value when n === 0.

//Iterative by recursion
var iterRFib = function(n){
  var helper = function(a, b, n){
    return n<2? b : helper(b, a+b, n-1);
  };
  return helper(0, 1, n);
};

We get a big performance jump using this approach. I got up to n = 13974 before reaching the max call size, and the function still returned in less than 0.1s. This time our time complexity is just O(n). Our space complexity is still O(n), as we must store each call to helper in our stack. This is why this function performs so much more quickly, but still runs into the call stack max.

Iterative Recursion

True Iteration

Unlike the approach above, the approach using JavaScript’s built in loop functionality does not rely on any recursive calls, and there for does not need to store all previous calls in memory. The code below stores the previous two values in an array (note, this could have been done in two separate variables as well), then enters a for loop that decrements from n to n === 2. Inside of the loop, we are setting the second value to the sum of the previous two values, and the first value to the previous second value. When n === 1, we exit the loop and return the second value.

//Truly iterative
var trueIterFib = function(n){
  var cache = [0, 1];
  for (n; n>1; n--){
    cache[2] = cache[1];
    cache[1] += cache[0];
    cache[0] = cache[2];
  }
  return cache[n];
};
True Iteration

Like the iterative recursion approach, this function performs small(ish) numbers blazingly fast compared to the true recursive approach (trueIterFib(14000) took less than 0.1s). However, it can also handle extremely large values without hitting the max call stack, because it has a space complexity of O(1)—it only needs to store the last two values at any given time, leaving a much smaller footprint. I was able to calculate the value of n = 1000000000 using the function with no problems–it took 4.2s though, which makes sense given that the time complexity is O(n).

This is the clear winner so far in terms of performance: O(n) time complexity and O(1) space complexity. But for argument’s sake, let’s say you were to write a program that repeatedly calcualted large Fibonnaci numbers. Even our trueIterFib function would prove unweildy for this situation. This is where memoization comes in.

Recursion with memoization

Let’s return to our original recursive solution, but add on three key lines of code. This time, we’re attaching the property memRFib to our function object, which we will use to cache previously computed values. This will allow us to “trim” a bunch of branches off of the recursion tree represented above. When we enter our function, we first check to see if we already have the value for n stored in our cache (memRFib); if we do, we simply return it. Otherwise, we enter our recursion the same as before, but store the results in the cache instead of returning them. Finally, we return the value of memRFib.mem[n].

//Recursive with memoization
var memRFib = function(n){
  if (!memRFib.mem[n]){
    memRFib.mem[n] = n<2 ? n : memRFib(n-1) + memRFib(n-2);
  }
  return memRFib.mem[n];
};
memRFib.mem = [];

Although we are still using recursion, both our time and space complexities are now O(n). The function must only calculate the value for each n once, which gives us a huge speed boost. Interestingly, we have the same space and time complexity as our iterative recursive strategy, and the performance of the two functions is almost identical (big asterisk asterisk!!! see below): I was able to calculate the value for n = 13962 before hitting the max call stack, and it took less than 0.1s. I would imagine that the small decrease in performance is likely due to the fact that we must store both the function calls and the memRFib.mem array here.

Recursion with memoization

Another interesting thing about this function is that while it will run into call stack issues if you try a large number at the beginning, once you’ve memoized, the limit increases. This means that theoretically the call stack max could be avoided with clever function calls.

Iteration with memoization

Alright, so how about we do something similar with our truly iterative function–it had the best performance before, will this improve it even more?

As with the recursion with memoization, here we add a property with which to store our results. We then check to see if we have already calculation the value, and if so, return that value. Otherwise, we enter our for loop, where we do something a bit more curious. We check again to see if the value has already been calculated before doing so. I’ll leave it to you to figure out why.

//Iterative memoization
var memIterFib = function(n){
  if (!memIterFib.mem[n]){
    for (var i = 2; i <=n; i++) {
      if (!memIterFib.mem[i]){
        memIterFib.mem[i] = memIterFib.mem[i-1] + memIterFib.mem[i-2];
      }
    }
  }
  return memIterFib.mem[n];
};
memIterFib.mem = [0, 1];
Iteration with memoization

Notably, thought we don’t run into any call stack errors, this version of our function actually performs much more slowly than the trueIter function. It took 4.2s to calculate the value of n = 17000000; I’m guessing this is because of the addition of the conditionals and lookups in this function. Additionally, while trueIter has a space complexity of O(1), this version has a space complexity of O(n) because we store all of our values in memIterFib.mem. So why on earth would we ever want to use this version? Keep reading for the answer.

Formula

Incidentally, there is also a formula one can use to calculate the nth Fibonnaci number:

an = (Phin - phin)/Sqrt(5)

where Phi = (1+Sqrt(5))/2 and phi = (1-Sqrt(5))/2

We can write this in code like so:

var fibEquation = function(n){
  var Phi = (1 + Math.sqrt(5))/2;
  var phi = (1 - Math.sqrt(5))/2;
  return (Math.pow(Phi, n) - Math.pow(phi, n))/Math.sqrt(5);
};

This option gives us O(1) for both the time and space complexity. Pretty sweet! It was able to calculate the value when n = 100000000000000000 in less 0.1s (I didn’t bother testing anything larger than this).

Summing it all up

So far, our functions rank in the following order based on their space and time complexities, as well as their actual performance: rFib < iterRFib ~ memRFib < memIterFib < trueIterFib

However, our tests have not taken into account situations in which you might want to repeatedly calculate various Fibonnaci numbers many times—this is where our memoized functions shine. To test this out, I modified this online tool and got the follow results.

Test specs

Whoa! A few interesting things are happening here. 1.) As before, rFib performs significantly slower than the rest of our functions. 2.) trueIterFib and iterRFib are comparable in these tests. This is exactly what we would guess would happen, given that these tests only use n = 20; this is far below the maximum n I was able to test before hitting the call stack max for iterRFib. 3.) Both memoized functions vastly outperform the rest of the functions, and are almost identical in their outcomes.This is because as soon as we test n = 20 once, this value is stored in the cache, so the function simply looks up the previously calculated value on each successive test. 4.) Though still fast, the formula was actually significantly slower than the memoized functions. This is because even though this function has a time complexity of O(1), it still requires more calculations each time than the memoized versions do after they calculate the value once.

So what does this mean? Well, we should take into account both the worst and best case scenarios when considering space and time complexity and choosing which function is “best” for our needs.

Approach Best Case Worst Case Notes
  Time Space TimeSpace  
Basic Recursion O(2n) O(n) O(2n) O(n) Easy to read. Very resource intensive
Iterative Recursion O(n) O(n) O(n) O(n) Uses a helper function Quick, but runs into call stack max
True Iteration O(n) O(1) O(n) O(1) Quick, even with very large values
Recursion with Memoization O(n) O(n) O(1) O(n) Quick, but can run into call stack max
Iteration with Memoization O(n) O(n) O(1) O(n) Fast for previously memoized values
Formula O(1) O(1) O(1) O(1) Extremely fast at calculating large values

And that. Is that.

Written on October 18, 2015