Fibonacci Optimisation
Just some stuff I thought about today. Here’s a fibonacci function expressed in Lisp:
-
(defun fibonacci (n)
-
(if (<= n 1)
-
n
-
(+ (fibonacci (- n 1)) (fibonacci (- n 2)))))
It can be used to compute the fibonacci sequence:
-
=> (mapcar #’fibonacci ‘(1 2 3 4 5 6 7 8 9 10))
-
(1 1 2 3 5 8 13 21 34 55)
It’s simplicity is elegant, but, frightfully inefficient. In fact, the recursive definition of fibonacci is often used as an example of an inefficient method. Let’s look at why it’s poor. When fibonacci is called for a value of n there are two possible paths the program can take:
- If n <= 1 give back the value of n
- otherwise return the sum of the previous two numbers in the sequence
So, if n is 0 or 1 then the value is simply 0 or 1 respectively. This is the base case of the function. In other words, from this piece of concrete knowledge, we can derive the remaining values. If n is any value higher than 1, then we need to actually compute what it is. Notice that this case requires fibonacci to be called another two times.
This imposes significant overhead on the machine. In addition to this, the results of these two function calls must be added together. That means we need to hold onto some space in memory to store the results. Simplistically, we refer to the number of significant function calls as the time complexity of the program, and the amount of memory required as the space complexity. Fibonacci sure is expensive.
Anyway, this got me to thinking. What is the main problem with fibonacci? It’s the fact that it must repeat a lot of its calculations. If we want to find the value of Fib(5), we must call Fib(4) and Fib(3). But wait: To find the value of Fib(4) we must call Fib(3). Hence, we do the same operation twice. What if we could tell the program to remember a value once it had been found? Wouldn’t this speed things up? Can this be done without sacrificing the simplicity of a recursive method?
Here’s a modified, partial fibonacci function:
-
(defun hashed-fibonacci (n)
-
(defvar fib-lookup (make-hash-table))
-
(let ((val (gethash n fib-lookup)))
-
(cond ((not (equal val nil)) val)
-
((<= n 1) n)
-
(t (let ((new-val (+ (hashed-fibonacci (- n 1)) (hashed-fibonacci (- n 2)))))
-
(progn (puthash n fib-lookup nil new-val)
-
new-val))))))
This one is a little more involved, but don’t panic: It still does the same thing, in much the same way. It has an extra trick though: Before it attempts to compute the value of n, it check to see if it already has a value. It looks in a structure called a hash table (any type of lookup table should do). Think of this like a telephone directory. If you know someone’s name, you can look it up in a phone directory and it will tell you their number. In our hash table, if you know a value of n, you can look it up and find its fibonacci value. Of course, some fibonacci values won’t be listed just yet. If there is no value in the table, we continue with our normal process: If n <= 2 then return 1. If n is greater than 2 then we find it’s value by adding the previous 2 fibonacci numbers. Now we have one more change to make: Having just found this new value of Fib(n) we store it in the hashtable. This means we won’t have to do the hard work of finding it again later – It’s already there!
Now, this act of remembering our previous work means that we must use a bit more memory, but the gain is a remarkable improvement in speed. Today, memory is pretty cheap, so this is not a bad thing. Look at the method we used. We start with a function that must call itself with a smaller value in order to discover a new value. When we discover a new value, we store it in a table so that we can quickly find it later.
This could most likely be generalised for any function that follows this pattern. There’s another blog post for when I get time to look into that aspect of it. If this post was about the optimisation, the next will be about abstracting that into a common pattern.
I hope this was of some interest. It was just a little thing I thought about this afternoon then decided to try it out. After I tried it I thought it might make a quick, informal blog entry.
Update: It seems that I have inadvertently stumbled across the definition of memoization which was conceived in 1968! It’s fun to try something out and then learn about a formal definition of the same principle. The act of developing an example gives it more meaning. (Further reading: http://en.wikipedia.org/wiki/Memoization).
