Let’s look in detail at a naïve—that is, simple and elegant, but computationally expensive (you’ll get O(2^n)) way to implement Fibonacci. This, perhaps, was the way you learned how to implement it when you first came across the idea of recursive programming.
Fibonacci is defined very simply:
- Fib(0) is 1
- Fib(1) is 1
- Fib(n) is the sum of Fib(n-1) and Fib(n-2) for all other n >=2
This last step is what makes it recursive—and, unfortunately,
what makes the elegant recursive solution. Let’s look at why.
The following is as literal (and correct) a translation of
the 3 bullet points above as possible:
public static int fibonacci(int n) {
if (n == 0 || n == 1) {
return n;
}
return fibonacci(n
- 1) + fibonacci(n - 2);
}
There’s a problem with this code; even if not in theory, in practice: It
repeats a lot of
work unnecessarily.
Suppose we need fib(10). Then we need
·
Fib(9)
o
For which we need fib(8)
§
For which we need fib(7)
·
For which we need fib(6)
·
And fib(5)
§
And fib(6)
·
For which we need fib(5)
·
And fib(4)
o and
fib(7)
· Fib(8)
o
for which we need fib(7)
o
and fib(6)
The tree of calls isn’t even complete—this is why Fibonacci
takes so long, O(2^n)—and look at how many times I’m repeating that I need royal
blue, or turquoise, or any other color. And realize that, if, for example, I
need fib(100), I need to do that whole tree’s worth of calculation
before I can find the answer, even if fib(100) is just an intermediary goal.
Of course, using fib(100) is quite an extreme example, but it does its job:
show just how much work you repeat due to the exponential nature of the problem.
Let’s actually look at a tree showing the sequence of calls for fib(7) to see
just how much work is repeated, on our way, in the next article, to a better
(if less obvious and elegant) way to calculate Fibonacci that saves orders of
magnitude of time and memory:
Fib(7), by definition, requires fib(6) and fib(5). But here’s
the problem: the traditional approach is to calculate fib(6) and throw
away the intermediate results, including an answer we need anyway before
calculating fib(5), which was already part of fib(6) but which we foolishly
threw away.
We get the O(2^n) complexity from the fact that the naïve formula splits us up
into 2 scenarios and has us calculate each one independently, and we see that
given the branching of the tree. 2^n grows very, very quickly, so while this
approach always remains correct, very quickly, it degrades in efficiency, so
the method we’ll propose in the next article takes over as the best way to
approach this problem.
No comments:
Post a Comment