Fibonacci numbers: the slow way or the fast and lazy way

Fibonacci numbers are a series of numbers such as 1,1,2,3,5,8,13,21,34,55,….

You can write a simple program, such as the one given below, in any language such as perl, python, ruby, javascript, php, java, haskell etc.

slowFib 0 = 0
slowFib 1 = 1
slowFib n = slowFib (n – 2 ) + slowFib (n – 1)

This algorithm is extremely slow, in any language. It takes exponential time — 2^n

This program was compiled to binary by the haskell compiler ghc. I am not showing the wrapper that takes user input and calls slowFib.

The CPU time taken is shown below. (Core 2 Duo 2.3 GHZ, 2 Gig RAM)

time ./slowfib 41
real 0m17.375s

time ./slowfib 42
real 0m28.109s

time ./slowfib 43
real 0m45.465s

One of the CPUs was pegged to 100% usage.
To compute 43rd Fibonacci number, you compute the 42nd Fibonacci number (takes 28 seconds) and add it to the 41st Fibonacci number (takes 17 seconds). So it takes 28+17 = 45 seconds. Calculations done for the 41st Fibonacci number are repeated for 42nd number which are again repeated for 43rd number and so on and that is the reason for the inefficiency.

If you want the 100th Fibonacci number, you will have to wait for a long, long time.

We can do lot better than this.
Let us write the first 10 Fibonacci numbers:

1   1   2   3   5   8  13  21  34  55

Now, skip the first number and write the remaining Fibonacci numbers:

1   2   3   5   8  13  21  34  55

Now let’s put them in 2 rows and add the columns:

1   1   2   3   5    8  13  21  34  55
1   2   3   5   8   13  21  34  55
2   3   5   8  13   21  34  55  89

Now, the sum is nothing but the Fibonacci sequence except that first 2 Fibonacci numbers 1 and 1 are missing. The first row is the Fibonacci sequence we are interested in. The second row is the tail of the Fibonacci sequence. Tail is the list without the first element. The sum is the tail of the tail of the Fibonacci sequence.
Initially, we have only the first 2 Fibonacci numbers, 1 and 1. So the 2 rows will look like this:

1 1

The second row has only one element as it is the tail of the first list which has only 2 elements.

Now let us add the 2 rows

1 1

But we know that the sum is the tail of the tail of the list. Tail of the Tail, as we have seen before, is where we remove the first 2 elements. This means that 2 should be third entry in the Fibonacci list. Let us append it to the first row:

1 1 2

Now that the first row has 3 elements, the second row (which is the tail of the first row) can have 2 elements.

1 1 2
1 2

Now, we can sum the second column

1 1 2
1 2
2 3

The sum, as we said before, is the tail of the tail of the Fibonacci sequence and it has one more number now — 3. Repeating the procedure described above, we get

1 1 2 3
1 2 3
2 3 5

which again becomes

1 1 2 3 5
1 2 3 5
2 3 5 8

and so on. The first row, which is the Fibonacci sequence, which started as 1,1 has now become 1,1,2,3,5. The operations we are doing are basically additions of two lists. We can stop this process as soon get the Fibonacci number we are interested in.

Now, we cannot (at least easily) write such a program in languages such as perl, python, ruby, javascript, php, java. These languages are called strict languages. To write such a program, you need a lazy language like Haskell. Lazy languages don’t compute something unless it is required. For example in haskell you can define

nines = 9:nines

which is an infinite list of numbers where every number is 9. Strict languages, seeing this recursive definition, will keep expanding nines until they run out of memory. Haskell, being a lazy language, won’t do anything. A lazy person like me can truly identify with this!
When you ask Haskell to compute “take 4 nines” it will return [9,9,9,9]. If you ask´┐Ż “take 8 nines”, you will get [9,9,9,9,9,9,9,9] and so on.
So, in haskell, you can write the method where we add 2 lists as

fibs = 1:1:zipWith (+) fibs (tail fibs)

A one line program that computes the Fibonacci series — blazingly fast!
Let us dissect the program 1:1:zipWith (+) fibs (tail fibs)

Remember, we started out with the first row having 1 and 1. That is what is shown here a 1:1.
zipWith (+)” essentially adds 2 lists. For example, zipWith (+) [1,2,3] [4,5,6] will give you [5,7,9].
So we are using zipWith to (lazily) add the Fibonacci list with the tail of the Fibonacci list, as was described earlier.

Now, if you ask Haskell to evaluate fibs, it will start printing all the Fibonacci numbers and the program will never stop until it runs out of memory. Haskell, being a lazy language, will only compute what is required and in this case you are asking it to print all the numbers. However, if you ask it for the first 10 Fibonacci numbers, like this “take 10 fibs”, it will give you “[1,1,2,3,5,8,13,21,34,55]”, computing only what is required.

If we want just the nth Fibonacci number, we can call “take n fibs” and take the last number in the list. Here’s the output of this program.

time ./fibs 43
real 0m0.002s

The program took hardly any time. Compare this with 45 seconds taken by the slow version. Now, for fun, let us compute the 5000th Fibonacci number. Let us not even think of doing it with the old algorithm as it will take over thousands of years to complete!

time ./fibs 5000

real 0m0.005s

The program took hardly any time!. How about the 10000 Fibonacci number? Takes hardly any time. The right algorithm makes all the difference. And, in this case, a lazy algorithm matched perfectly with Haskell’s lazy evaluation, and the problem was solved with a one line program!

time ./fibs 10000

real 0m0.010s

As Dana Carvey would say “Well, isn’t that special!”
For more info on lazy evaluation in Haskell, look at the 14th chapter in Paul Hudak’s book. There is also a free online Beta book called Real World Haskell, to be published soon by O’Reilly. The online version has been a work in progress for about a year and is almost complete.

Leave a Reply

Your email address will not be published. Required fields are marked *