1

Here is the code I currently have.

def fibonacci(n):

    if n == 1:
        return 1
    elif n == 2:
        return 1
    else:
        value = fibonacci(n - 1) + fibonacci(n - 2)
        return value

This currently takes quite some time to calculate values greater than n = 30. Is there a more computationally efficient method to accomplish this?

martineau
  • 119,623
  • 25
  • 170
  • 301
James Geddes
  • 742
  • 3
  • 10
  • 35

4 Answers4

3

Adding a value cache to trade some memory for a reduced processing time can be a useful method. A purely recursive program will attempt to calculate values over and over again, however this takes time for larger values. If the values do not change, then storing them can be helpful. It is important to note, however, that should values be volatile you might need a different approach.

fibonacci_value_cache = {}


def fibonacci(n):

    if n == 1:
        return 1
    elif n == 2:
        return 1
    elif n in fibonacci_value_cache:
        return fibonacci_value_cache[n]
    else:
        fibonacci_value_cache[n] = fibonacci(n - 1) + fibonacci(n - 2)
        return fibonacci_value_cache[n]

n = 100

print("Fib " + str(n) + ": " + str(fibonacci(n)))

Here, we check if the value is in the dictionary and return it if it is, otherwise we calculate it and add it to the dictionary. This means that we are make better use of the processor by not calculating the same value multiple times.

James Geddes
  • 742
  • 3
  • 10
  • 35
  • 3
    Bonus tip: For Python 3 users, the [`lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache) decorator conveniently does all the work of caching your values, without you having to manually write dict management conditionals yourself. – Kevin Oct 05 '17 at 15:28
  • I do love Python! So much of the awesomes! – James Geddes Oct 05 '17 at 15:33
  • 1
    FWIW, if you want a fast way to calculate Fibonacci numbers please see `fast_fib` near the end of my answer [here](https://stackoverflow.com/a/40683466/4014959) – PM 2Ring Oct 05 '17 at 15:39
  • 1
    BTW, it's better to let `print` (or `format`) handle the string conversion & concatenation for you, rather than doing it manually. Eg, `print("Fib", n, fibonacci(n))` or `print('Fib {} {}'.format(n, fibonacci(n)))`. The code tends to be more compact as well as more efficient. – PM 2Ring Oct 05 '17 at 15:43
  • 2
    You also have two if-branches that do the same thing with conditions that can easily be covered by a single condition. – user2390182 Oct 05 '17 at 15:44
  • 2
    And I guess it ought to be mentioned that while doing Fibonacci calculations may be a good exercise in learning recursion it's generally best to avoid recursion in Python, unless you're doing something that actually needs recursion, eg processing recursive data structures like trees. Python cannot optimize [tail calls](https://en.wikipedia.org/wiki/Tail_call), and it has a limit to its recursion depth (although you can change that limit if you need to). – PM 2Ring Oct 05 '17 at 15:51
  • 1
    James: If you initialized the cache dictionary with `fibonacci_value_cache = {0: 0, 1: 1}` then you could eliminate the first couple of `if`s at the beginning of your function. – martineau Oct 06 '17 at 20:05
0

Using idea of Dynamic Programming, and store the intermediate results to save computational cost, it could be very efficient. The code below cost less than 0.02s for n=10000 on my laptop.

def fib(n):  # return Fibonacci series up to n
    result = []
    a, b = 0, 1
    for i in range(n):
        result.append(b)
        a, b = b, a + b
    return result
mutux
  • 352
  • 3
  • 12
  • You code isn't recursively calculating the Fibonacci sequence, which the question specifically asks about, not doing it some other different way. – martineau Oct 05 '17 at 15:55
  • Ops, didn't see that, recursion is costive though. – mutux Oct 05 '17 at 16:03
  • You should use a for-loop here. – juanpa.arrivillaga Oct 05 '17 at 16:03
  • You're right, could look more elegant with for-loop. I'm modifying it. – mutux Oct 05 '17 at 16:08
  • @juanpa: Maybe not a for-loop here. It's not necessary to check every `i` value up to `n` incrementing it by `1` each time. It can be increased to at least `b` each iteration. Should make it even faster... – martineau Oct 05 '17 at 16:09
  • `for in in range(n):` is a `SyntaxError` (`in` is a keyword in Python). Also, your for-loop change does not speed anything up because the loop (if corrected) would still iterate `n` times (and computes a total of `n` numbers in the Fibonnaci series, not numbers in it up to `n`). – martineau Oct 05 '17 at 16:24
  • Thanks @martineau, it's a typo. There's no significant improvement indeed, just it looks more elegant than before as I said. Generally, visiting memory is faster than computing though. The idea you mentioned above sounds interesting, I would like figure it out, could you please explain it a bit or indicate a reference for that? – mutux Oct 05 '17 at 16:36
  • No reference...just common sense. If you initialize `result = [0]`, then the loop can simply be `while b <= n:` which is both correct (in the get Fibonnaci series numbers up to `n` sense) and super fast because it skips from one Fibonnaci number in the sequence to the next. – martineau Oct 05 '17 at 16:49
  • Well, then we're talking about different cases. `n` is actually used as the index of the F series in the code above, not as a upper boundary as in your case. Also, I didn't see skips according to your explanation. – mutux Oct 05 '17 at 17:06
0

There's a recipe for a decorator that uses as an example exactly what you want. It's named Memoize in the PythonDecoratorLibrary.

It may seem like overkill, but having the memoized decorator around could be useful for other future tasks. That said, here it is in its entirety (although I changed the print at the end):

import collections
import functools

class memoized(object):
   '''Decorator. Caches a function's return value each time it is called.
   If called later with the same arguments, the cached value is returned
   (not reevaluated).
   '''
   def __init__(self, func):
      self.func = func
      self.cache = {}
   def __call__(self, *args):
      if not isinstance(args, collections.Hashable):
         # uncacheable. a list, for instance.
         # better to not cache than blow up.
         return self.func(*args)
      if args in self.cache:
         return self.cache[args]
      else:
         value = self.func(*args)
         self.cache[args] = value
         return value
   def __repr__(self):
      '''Return the function's docstring.'''
      return self.func.__doc__
   def __get__(self, obj, objtype):
      '''Support instance methods.'''
      return functools.partial(self.__call__, obj)

@memoized
def fibonacci(n):
   "Return the nth fibonacci number."
   if n in (0, 1):
      return n
   return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(12))
martineau
  • 119,623
  • 25
  • 170
  • 301
0

No need for caching/memoization. Here's a Python 3 implementation that expresses the Fibonacci sequence as powers of a matrix, then does efficient exponentiation via halving and squaring. The result is O(log n) in both time and storage.

def matrix_fib(n):
    if n == 1:
        return [0,1]
    else:
        f = matrix_fib(n // 2)
        c = f[0] * f[0] + f[1] * f[1]
        d = f[1] * (f[1] + 2 * f[0])
        return [c,d] if (n & 1) == 0 else [d,c+d]

def fib(n):
  return n if n == 0 else matrix_fib(n)[1]

print(fib(1000000))

On my laptop this coughs up the value of the millionth Fibonacci number in a little over half a second, and the bulk of that is probably in the big integer arithmetic and formatting of the output—the result is ridiculously large. You don't need to worry about stack overflow, though. The call stack depth for this is only log2(1000000) = 20.

pjs
  • 18,696
  • 4
  • 27
  • 56