By: Jan Vanhove
Re-posted from: https://janhove.github.io/posts/2022-12-20-julia-fibonacci/index.html
I’m currently learning a bit of Julia and I thought I’d share with you a couple of my attempts at writing Julia code. I’ll spare you the sales pitch, and I’ll skip straight to the goal of this blog post: writing three different Julia functions that can generate the Fibonacci sequence.
The Fibonacci sequence
The famous Fibonacci sequence is an infinite sequence of natural numbers, the first of which are 1, 1, 2, 3, 5, 8, 13, …. The sequence is defined as follows:
Let’s write some Julia functions that can generate this sequence.
Julia
You can download Julia from julialang.org. I’m currently using the Pluto.jl package that allows you to write Julia code in a reactive notebook. Check out the Pluto.jl page for more information.
First alternative: A purely recursive function
The Fibonacci sequence is defined recursively: To obtain the nth Fibonacci number, you first need to compute the n-1th and n-2th Fibonacci number and then add them. We can write a Julia function that exactly reflects the definition of the Fibonacci sequence like so:
function fibonacci(n)
if n <= 2
return 1
end
return fibonacci(n - 1) + fibonacci(n - 2)
end
fibonacci (generic function with 1 method)
This function tacitly assumes that n
is a non-zero natural number. If n
is equal to or lower than 2, i.e., if n
is 1 or 2, it immediately returns 1, as per the definition of the sequence. If this condition isn’t met, the output is computed recursively. The function can be run as follows:
Checks out! But from a computational point of view, the fibonacci()
function is quite wasteful. In order to obtain fibonacci(10)
, we need to compute fibonacci(9)
and fibonacci(8)
. But in order to compute fibonacci(9)
, we also need to compute fibonacci(8)
. For both fibonacci(9)
and fibonacci(8)
, we need to compute fibonacci(7)
, etc. In fact, we need to compute the value of fibonacci(8)
two times, that of fibonacci(7)
three times, that of fibonacci(6)
five times, and that of fibonacci(5)
seven times. So we’d be doing lots of computations over and over again. For this reason, the fibonacci()
function is hopelessly inefficient: While you can compute fibonacci(10)
in a fraction of a second, it may take minutes to compute, say, fibonacci(60)
. Luckily, we can speed up our function considerably.
Second alternative: Recursion with memoisation
Memoisation is a programming technique where any intermediate result that you computed is stored in an array. Before computing any further intermediate results, you then first look up in the array if you haven’t in fact already computed it, saving you a lot of unnecessary computations. The following Julia function is a bit more involved that the previous one, but it’s much more efficient.
function fib_memo(n)
known = zeros(Int64, n)
function memoize(k)
if known[k] != 0
# do nothing
elseif k == 1 || k == 2
known[k] = 1
else
known[k] = memoize(k-1) + memoize(k-2)
end
return known[k]
end
return memoize(n)
end
fib_memo (generic function with 1 method)
The overall function that we’ll actually call is fib_memo()
. It creates an array called known
with n
zeroes. Then it defines an inner function memoize()
. This latter function obtains an integer k
that in practice will range from 0 to n
and does the following. First, it checks if the k
th value in the array known
is still 0. If it got changed, the function just returns the k
th value in known
. Otherwise, if k
is equal to either 1 or 2, it sets the first or second value of known
to 1. If k
is greater than 2, the k
th value of known
is computed recursively. In all cases, the memoize()
function returns the k
value of the known
array. The outer fib_memo()
function then just returns the result of memoize(n)
.
Perhaps by now, your computer has finished running fibonacci(60)
and you can try out the alternative implementation:
Notice how much faster this new function is! Even the 200th Fibonacci number can be computed in a fraction of a second:
Unfortunately, we’ve ran into a different problem now: integer overflow. The result of the computations has become so large that it exceeded the range of 64-bit integers. To fix this problem, we can work with BigIntegers instead:
function fib_memo(n)
known = zeros(BigInt, n)
function memoize(k)
if known[k] != 0
# do nothing
elseif k == 1 || k == 2
known[k] = 1
else
known[k] = memoize(k-1) + memoize(k-2)
end
return known[k]
end
return memoize(n)
end
fib_memo (generic function with 1 method)
280571172992510140037611932413038677189525
Nice!