A Fibonacci number is the sum of the two previous numbers in the sequence. Write a function that finds the n-th Fibonacci number.
These are the first 10 fibonacci numbers: 1 1 2 3 5 8 13 21 34 55
Input: 5 Output: 5 Input: 12 Output: 144 Input: 200 Output: 280571172992510140037611932413038677189525
The number of bits needed to represent the n-th fibonacci number scales linearly with n, so we need to consider an extra O(n) factor when considering time/space complexities.
For reference:
Solution | Time | Space |
---|---|---|
Recursion | O(2n n2) | O(n2) |
Memoization | O(n2) | O(n2) |
Dynamic programming | O(n2) | O(n2) |
Iterative | O(n2) | O(n) |
Binet's formula | O(n log n log log n) to O(n2) | O(n) |
Fast doubling | O(n log n log log n) to O(n2) | O(n) |
def fibonacci(n) return n if n <= 1 fibonacci(n - 1) + fibonacci(n - 2) end
Time | O(2n n2) | 2 recursive calls adding O(n)-bit numbers at n levels |
Space | O(n2) | O(n) max stack depth with O(n) bits for the nth number |
def fibonacci(n, cache = []) return n if n <= 1 return cache[n] unless cache[n].nil? cache[n] = fibonacci(n - 1, cache) + fibonacci(n - 2, cache) end
Time | O(n2) | O(n) stack depth adding O(n) bit numbers |
Space | O(n2) | n-sized array of numbers with O(n) bits per number |
def fibonacci(n) numbers = Array.new(n + 1) numbers[0] = 0 numbers[1] = 1 (2 .. n).each do |i| numbers[i] = numbers[i - 1] + numbers[i - 2] end numbers[n] end
Time | O(n2) | O(n) loop adding O(n)-bit numbers at each iteration |
Space | O(n2) | n-sized array of numbers with O(n) bits per number |
def fibonacci(n) a, b = 0, 1 n.times do a, b = b, a + b end a end
Time | O(n2) | O(n) loop adding O(n) bit numbers at each iteration |
Space | O(n) | Two integers each containing O(n) bits |
Calculating a fibonacci number with this formula uses log(n) multiplications when using exponentiation by squaring, which results in a time complexity bounded by the runtime of the multiplication algorithm being used:
Runtime | Algorithm |
---|---|
O(n2) | Regular long multiplication |
O(n1.585) | Karatsuba algorithm |
O(n1.465) | 3-way Toom-Cook multiplication |
O(n log n log log n) | Schonhage-Strassen algorithm |
In the worst case, multiplication is done by multiplying each digit together and summing them up. The runtime is O(n2), but in practice this algorithm is slower than the O(n2) addition algorithms because of performance penalties in floating-point arithmetic vs adding integers.
The best case happens with the Schonhage-Strassen algorithm, which starts to outperform the other algorithms at astronomically large values of n with tens-of-thousands of digits.
The floating point precision of standard libraries in most programming languages are not enough to handle the precision needed for large numbers of n. You'll need to use a library designed to handle unlimited precision like the GNU multiple precision arithmetic library if you want accurate calculations as n increases in size.
require 'gmp' def fibonacci(n) precision = [16, (0.8*n).to_i].max sqrt_5 = GMP::F.new(5, precision).sqrt phi = (sqrt_5 + 1) / 2 GMP::Z.new(phi**n / sqrt_5) end
Time | O(n log n log log n) to O(n2) | Varies depending on the multiplication algorithm used |
Space | O(n) | Floating point numbers each of size O(n) |
Same runtime as Binet's formula above, but operating only on integers, so there's no need for an arbitrary floating point precision library.
def fibonacci(n) tuples = lambda do |n| return [0, 1] if n == 0 a, b = tuples.call(n / 2) c = a * (b * 2 - a) d = a ** 2 + b ** 2 if n % 2 == 0 [c, d] else [d, c + d] end end tuples.call(n).first end
Time | O(n log n log log n) to O(n2) | Varies depending on the multiplication algorithm used |
Space | O(n) | Integers of size O(n) |