Skip to content

Instantly share code, notes, and snippets.

@AlexandraBeautyman
Last active June 25, 2019 02:53
Show Gist options
  • Save AlexandraBeautyman/ffae2c66cd3e5622928d0554e11fa4f9 to your computer and use it in GitHub Desktop.
Save AlexandraBeautyman/ffae2c66cd3e5622928d0554e11fa4f9 to your computer and use it in GitHub Desktop.

Prompt

First, given the (quite inefficient) algorithm for calculating the nth Fibonacci number below, determine its time complexity.

function fib(n) {
  if (n === 0) {
    return 0
  }
  else if (n === 1) {
    return 1
  }
  return (fib(n - 1) + fib(n - 2))
}

Second, given this additional algorithm for printing out all the Fibonacci numbers from 0 to n, determine its time complexity.

function allFib(n) {
  for (let i = 0; i <= n; i++) {
    console.log(fib(i))
  }
}

Approach

In the first part of the prompt, we should notice that the bulk of the time complexity in the function happens in the line when we return the sum of two recursive function calls. Since we have to work our way down from n to 0 step by step, you might at first think that this is a simple case of O(2n) => O(n). But notice that at each recursive step, we are not merely continuing to make two calls. Instead, the number of calls doubles at each step. (Those two recursive calls will each make two recursive calls, which will each make two recursive calls in turn.) So at first we are making two calls; then we are making four calls; then eight, etc.

This is a recognizable pattern! It's a binary tree. image

That tells us that the time complexity of this set of recursive calls is approximately proportional to 2^n. (How do we know this? See the section below titled "How many nodes are in a binary tree?") That makes the time complexity of the whole function O(2^n), since only constant steps take place other than the recursive calls, and Big O lets us ignore constants.

What about the second function? At first glance, it seems like the second function is calling the first function n times, and therefore should have a time complexity of O(n*2^n). But the value of "n" being fed into the function fib as an argument is actually changing at each step of the for loop in the function allFib. It starts at 0, then increases by 1 until reaching the "n" being fed as an argument into allFib.

So the time complexity of allFib is actually the sum of the time complexities of the fib calls for each value of i: 2^0 + 2^1 + 2^3 + ... 2^n. This sum is equal to 2^(n + 1) - 1. (How do we know this? See the section below titled "How many nodes are in a binary tree?") This reduces to a time complexity of O(2^n) when you get rid of the constants, so the time complexity for allFib is the same as the time complexity for fib.

How many nodes are in a binary tree?

You may have heard before that when you have a recursive function that makes multiple calls, the runtime will often look like O(branches^depth), where branches is the number of times each recursive call branches. But why is this true? To reason it out, let's look at the binary tree image above. It takes the shape of a binary tree because the original function includes a line in which it calls itself twice. In most cases, each of those calls will then run a line that makes another two calls of the function itself (for a total of four), and so on.

You can imagine that if a function were to call itself three times, the branching would be tertiary instead of binary: each of the three recursive calls would call the function three times, and so on.

The math is simpler to see in the binary case, so let's focus on that. We can first ask, how deep will the tree go? In the case of the fib function, we know that the input has to work its way down from n to 0 (roughly), along both paths, so we can expect the tree to have approximately a depth of n. We then ask, how many nodes occur at each depth? Because each function calls itself twice, we know that the number of nodes will double at each successive level. At the zeroith level, we have 1 node; at the first level, we have 2 nodes; at the second level, we have 4 nodes, etc. The mathematical relationship between the depth and the number of nodes at that depth is therefore 2^d, where d is the level of depth.

At the deepest level (the nth level), then, we would expect to find approximately 2^n nodes. It might seem a bit counter-intuitive, at this point, that the total number of nodes be approximately 2^n, since there are obviously more nodes in the whole tree than just in the bottom level. But let's do the math.

If we were to sum up all the nodes at all the levels, we would get 2^0 + 2^1 + 2^2 + ... 2^(n - 1) + 2^n. Looking at this sum conceptually, we can think of it as 2^n + (2^n)/2 + (2^n)/4 + ... 2 + 1. Working our way up from the bottom level, each level contributes half of the nodes from the next lowest level to the total. The image below illustrates how such a sum will never be greater than two times 2^n, since each level (moving up) will never quite fill the second square. image

In fact, if you work your way all the way down to 2^0, which equals 1, you will end up short of two times 2^n by exactly one, for a sum total of 2 * 2^(n) - 1, or 2^(n + 1) - 1 nodes. Recall that Big O doesn't care about constants, and this reduces to O(2^n).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment