Why is the Fibonacci Sequence Big O(2^n) instead of O(logn)?

29,450

Solution 1

The other answers are correct, but don't make it clear - where does the large difference between the Fibonacci algorithm and divide-and-conquer algorithms come from? Indeed, the shape of the recursion tree for both classes of functions is the same - it's a binary tree.

The trick to understand is actually very simple: consider the size of the recursion tree as a function of the input size n.

Recall some basic facts about binary trees first:

  • The number of leaves n is a binary tree is equal to the the number of non-leaf nodes plus one. The size of a binary tree is therefore 2n-1.
  • In a perfect binary tree, all non-leaf nodes have two children.
  • The height h for a perfect binary tree with n leaves is equal to log(n), for a random binary tree: h = O(log(n)), and for a degenerate binary tree h = n-1.

Intuitively:

  • For sorting an array of n elements with a recursive algorithm, the recursion tree has n leaves. It follows that the width of the tree is n, the height of the tree is O(log(n)) on the average and O(n) in the worst case.

  • For calculating a Fibonacci sequence element k with the recursive algorithm, the recursion tree has k levels (to see why, consider that fib(k) calls fib(k-1), which calls fib(k-2), and so on). It follows that height of the tree is k. To estimate a lower-bound on the width and number of nodes in the recursion tree, consider that since fib(k) also calls fib(k-2), therefore there is a perfect binary tree of height k/2 as part of the recursion tree. If extracted, that perfect subtree would have 2k/2 leaf nodes. So the width of the recursion tree is at least O(2^{k/2}) or, equivalently, 2^O(k).

The crucial difference is that:

  • for divide-and-conquer algorithms, the input size is the width of the binary tree.
  • for the Fibonnaci algorithm, the input size is it the height of the tree.

Therefore the number of nodes in the tree is O(n) in the first case, but 2^O(n) in the second. The Fibonacci tree is much larger compared to the input size.

You mention Master theorem; however, the theorem cannot be applied to analyze the complexity of Fibonacci because it only applies to algorithms where the input is actually divided at each level of recursion. Fibonacci does not divide the input; in fact, the functions at level i produce almost twice as much input for the next level i+1.

Solution 2

To address the core of the question, that is "why Fibonacci and not Mergesort", you should focus on this crucial difference:

  • The tree you get from Mergesort has N elements for each level, and there are log(n) levels.
  • The tree you get from Fibonacci has N levels because of the presence of F(n-1) in the formula for F(N), and the number of elements for each level can vary greatly: it can be very low (near the root, or near the lowest leaves) or very high. This, of course, is because of repeated computation of the same values.

To see what I mean by "repeated computation", look at the tree for the computation of F(6):

fib(6) computation tree
Fibonacci tree picture from: http://composingprograms.com/pages/28-efficiency.html

How many times do you see F(3) being computed?

Solution 3

With the recursive algo, you have approximately 2^N operations (additions) for fibonacci (N). Then it is O(2^N).

With a cache (memoization), you have approximately N operations, then it is O(N).

Algorithms with complexity O(N log N) are often a conjunction of iterate over every item (O(N)) , split recurse, and merge ... Split by 2 => you do log N recursions.

Solution 4

Consider the following implementation

int fib(int n)
{
    if(n < 2)
      return n;
    return fib(n-1) + fib(n-2)
}

Let's denote T(n) the number of operations that fib performs to calculate fib(n). Because fib(n) is calling fib(n-1) and fib(n-2), it means that T(n) is at least T(n-1) + T(n-2). This in turn means that T(n) > fib(n). There is a direct formula of fib(n) which is some constant to the power of n. Therefore T(n) is at least exponential. QED.

Solution 5

To my understanding, the mistake in your reasoning is that using a recursive implementation to evaluate f(n) where f denotes the Fibonacci sequence, the input size is reduced by a factor of 2 (or some other factor), which is not the case. Each call (except for the 'base cases' 0 and 1) uses exactly 2 recursive calls, as there is no possibility to re-use previously calculated values. In the light of the presentation of the master theorem on Wikipedia, the recurrence

f(n) = f (n-1) + f(n-2)

is a case for which the master theorem cannot be applied.

Share:
29,450
loremIpsum1771
Author by

loremIpsum1771

Updated on July 09, 2022

Comments

  • loremIpsum1771
    loremIpsum1771 almost 2 years

    I took discrete math (in which I learned about master theorem, Big Theta/Omega/O) a while ago and I seem to have forgotten the difference between O(logn) and O(2^n) (not in the theoretical sense of Big Oh). I generally understand that algorithms like merge and quick sort are O(nlogn) because they repeatedly divide the initial input array into sub arrays until each sub array is of size 1 before recursing back up the tree, giving a recursion tree that is of height logn + 1. But if you calculate the height of a recursive tree using n/b^x = 1 (when the size of the subproblem has become 1 as was stated in an answer here) it seems that you always get that the height of the tree is log(n).

    If you solve the Fibonacci sequence using recursion, I would think that you would also get a tree of size logn, but for some reason, the Big O of the algorithm is O(2^n). I was thinking that maybe the difference is because you have to remember all of the fib numbers for each subproblem to get the actual fib number meaning that the value at each node has to be recalled, but it seems that in merge sort, the value of each node has to be used (or at least sorted) as well. This is unlike binary search, however, where you only visit certain nodes based on comparisons made at each level of the tree so I think this is where the confusion is coming from.

    So specifically, what causes the Fibonacci sequence to have a different time complexity than algorithms like merge/quick sort?

  • Codor
    Codor over 8 years
    @greybeard Thanks for the remark; I have corrected the typo.
  • loremIpsum1771
    loremIpsum1771 over 8 years
    It seems that some of the other answers are suggesting that the difference is in the fact that algorithms like merge sort get to their base condition by reducing the input size by a factor for each recursive call while algorithms like Fibonacci and Towers of Hanoi only reduce the size by addition/subtraction each time, getting to the base case a lot slower. But you're saying that the repeated computations (which can be solved by DP/Memoization) is what causes the difference? Or is it both?
  • William
    William over 8 years
    The problem lies on the fact that the same problem is repeated. This is a recursive implementation of the sum of the first N numbers: S(N) = N + S(N-1), the base case being S(0) = 0. The reduction is by addition/subtraction, however this is O(N).
  • interjay
    interjay over 8 years
    The recursive algorithm is exponential, not quadratic. Where do you get n^2/2 from?
  • guillaume girod-vitouchkina
    guillaume girod-vitouchkina over 8 years
    @interjay - abolutely 2^N
  • William
    William over 8 years
    Notice that, of course, if you add memoization then Fibonacci becomes O(n).
  • interjay
    interjay over 8 years
    More accurately, about 1.618^n.
  • loremIpsum1771
    loremIpsum1771 over 8 years
    So because the fib sequence simply decrements for each recursive call, it height of the tree would be linear as opposed to logarithmic like for merge sort which repeatedly halves the input size. And the width of the tree is specifically **2**^k because there are two recursive calls in the return statement and because there are repeated subproblems being solved?
  • kfx
    kfx over 8 years
    Yes, there are k levels (because for each i, fib(i) evaluates fib(i-1)), and O(2^k) leaves, because i+1 level of the tree has approximately twice as many nodes as the i-th level.
  • loremIpsum1771
    loremIpsum1771 over 8 years
    Is drawing out the recursive tree the only way to know that the i+1 has twice as many nodes as the previous level? I'm just trying to get the intuition for the width?
  • kfx
    kfx over 8 years
    No, but the drawing makes things much easier. The important thing to notice is that each call of fib spawns two more calls of fib for all except very small values (<=2). Therefore the next level has twice as much fib calls. Same thing of course happens with mergesort, quicksort, etc. The shape of the recursion tree is basically the same (a binary tree) for many different functions.
  • loremIpsum1771
    loremIpsum1771 over 8 years
    Oh ok, I'm starting to get it now. So essentially, since each call makes 2 other calls at each tree level k, it is O(2^k) and it would be O(3^k) or O(4^k) if there were 3 or four calls, respectively,made at each node. And even though algorithms like merge sort have the same recursion trees as fib, it seems the difference is in the operation being done. Since fib requires the summing of all of the nodes on each level (which is 2^n), it is O(2^n). If divide and conquer algos required this summing as well, they would also be exponential right?
  • kfx
    kfx over 8 years
    I don't see why you think the operation would make a large difference. Actually, merging a list of sorted number is a far more complex more operation than adding two numbers, isn't it? Focus on the size of the tree - it's the necessary and sufficient difference. I'll expand the answer with a more rigorous math.
  • loremIpsum1771
    loremIpsum1771 over 8 years
    Makes sense, that's what I was getting at. Divide and Conquer Algorithms divide the input at each step while algorithms like Fibonacci don't, creating more input at each successive step, thus, growing the size (width) of the tree with each recursive call.