Play With Lua!

Fun with Fibonacci

without comments

Or, how to annihilate an interview question. I was asked this a little over a year ago in an interview, and I’d been asked it a few times before then too; it’s pretty common.

“Write a function that takes a parameter n and returns the nth Fibonacci number.” The Fibonacci sequence, if you don’t already know, is defined like this: f(1) and f(2) are both 1. After that, each number in the sequence is the sum of the previous two, so, f(3) is 2 (because 1+1), then f(4) is 3, then f(5) is 5, and so on.

(Fun, useless fact: this was originally discovered as part of a thought experiment about rabbits. If you imagine that you start with two rabbits, and each month each rabbit produces another rabbit (but rabbits don’t reproduce until they’re one month old), then the rabbit population after n months is the nth Fibonacci number.)

Naive version

So, start off by writing the naive recursive version:

function fib(n)
    if n < 3 then return 1
    else return fib(n-1) + fib(n-2) end
end

This works, but it’s inefficient: if you call it with n=4, say, it results in 5 calls: 4, 3, 2, 1, and 2. Calling it with n=8 is 21 calls. In fact, it’s actually O(fib(n)) complexity; the number of recursive calls rises at the same rate as the Fibonacci sequence itself.

So let’s make it faster.

Iterative version

One way is to make it iterative:

function ifib(n)
    local a,b = 1,1
    for k = 3,n do
        a, b = b, a+b
    end
    return b
end

Now it won’t recurse at all, of course, but it’s kind of ugly. Recursion is cool, iteration is boring. Let’s try to make the recursive one more efficient.

Memoized version

If you look at the naive version, it’s calling itself with the same argument several times. We can help matters by caching the results so after it calls once it never has to again:

function memoize(fn)
    local cache = {}
 
    return function(n)
        cache[n] = cache[n] or fn(n)
        return cache[n]
    end
end
 
fib = memoize(fib)

Now when you call it with n=4, say, you only get 4 recursive calls: 4, 3, 2, and 1. In general it will call itself n times, because if it ever has to repeat a call it’ll use the cache. So now it’s O(n).

But that’s not perfect. We’re still using a lot of stack space. It would be nicer if it ran in constant stack space.

Tail-recursive version

function tfib(n)
    local function f(a, b, n)
        if n < 3 then
            return b
        else
            return f(b, a+b, n-1)
        end
    end
 
    return f(1,1,n)
end

You can see this is a lot like the iterative version: we’re starting from the first two values and working up, and shifting (a+b)-to-b and b-to-a each time. But the mechanism of looping is recursion, and the recursive call is the last thing that happens in any given invocation, so, it’s tail recursive and Lua can eliminate the tail call so it never blows out the stack.

At this point we’ve got a naive, easy-to-read version; a version that’s fast and small but ugly; a version that’s fast and pretty but needs memory for a cache; and a version that’s fast and pretty and uses constant memory. The only way to improve it further is to be clever:

The closed-form version

The first three new versions we made are general transformations useful for speeding up any slow function. This one is specific to the Fibonacci function. It turns out that as n approaches infinity, the ratio of fib(n) to fib(n-1) is phi, the golden ratio.

Phi is (1 + math.sqrt(5)) / 2. The closest integer to phi^n / math.sqrt(5) is the nth Fibonacci number, and we can easily find that with math.floor:

phi = (1+math.sqrt(5)) / 2
 
function fib(n)
    return math.floor(phi^n / math.sqrt(5) + 1/2)
end

It doesn’t get much better than this: constant time and constant space. Beyond this there’s not a lot you can do to make it more efficient.

Written by randrews

February 5th, 2016 at 11:25 pm

Posted in Uncategorized