0

I calculate fibonachi row in two different ways. Why fib1 executes much longer then fib2?

public class RecursionTest {

    @Test
    public void fib1() {
        long t = System.currentTimeMillis();

        long fib = fib1(47);

        System.out.println(fib + "  Completed fib1 in:" + (System.currentTimeMillis() - t));

        t = System.currentTimeMillis();

        fib = fib2(47);

        System.out.println(fib + "  Completed fib2 in:" + (System.currentTimeMillis() - t));

    }


    long fib1(int n) {
        if (n == 0 || n == 1) {
            return n;
        } else {
            return fib1(n - 1) + fib1(n - 2);
        }
    }


    long fib2(int n) {
        return n == 0 ? 0 : fib2x(n, 0, 1);
    }

    long fib2x(int n, long p0, long p1) {
        return n == 1 ? p1 : fib2x(n - 1, p1, p0 + p1);
    }
}

The output:

2971215073  Completed fib1 in:17414
2971215073  Completed fib2 in:0
beerbajay
  • 19,652
  • 6
  • 58
  • 75
user590444
  • 4,252
  • 8
  • 36
  • 43
  • 5
    You should step through this with a debugger to see how it unfolds, but your first technique invokes 1025119359 method calls, while your second invokes just 47. – Beau Grantham Jun 07 '12 at 17:11
  • 2
    Yep, those algorithms are entirely different. One has runtime complexity `O(2^n)`, while the other is in `O(n)` – Niklas B. Jun 07 '12 at 17:12
  • And there are faster still algorithms: [here's one written in C++](http://ideone.com/dQtVl) – Mooing Duck Jun 07 '12 at 17:40
  • To be more tight, `fib1()` is `O(1.618^n)` since the right branch is called with n-2. – rettvest Jun 08 '12 at 18:04

4 Answers4

6

Because both algorithms work entirely different. Let me show you this with fib(5).

if you call fib1(5), it internally calls fib1(4) und fib1(3), lets visualize that with a tree:

    fib(5)
  /       \
fib(4)   fib(3)

now, fib(4) internally calls fib(3) and fib(2).

So now we have this:

           fib(5)
          /       \
    fib(4)       fib(3)
    /  \          /  \
fib(3) fib(2)  fib(2) fib(1)

I think by now it is very obvious where this is going, you should be able to fill in the rest.

edit: Another thing you should notice here is, that it actually has to performe the same caclculation multiple times. In this picture, fib(2) und fib(3) are both called multiple times. And this gets worse if the starting number is bigger./edit

Now, let's take a look at fib2(5). It you call it with 0, it returns 0. Otherwise, it calls fib2x(n, 0,1) So, we have a call to fib2x(5,0,1). fib2x(n, 0,1) now internally calls fib2x(n-1, p1, p0+p1) and so on. So, lets see:

        
fib2x(5, 0,1) => fib2x(4, 1,1) => fib2x(3, 1, 2) => fib2x(2, 2, 3) => fib2x(1, 3, 5)

by then, it has reached the return condition and returns 5.

So, your algorithms work entirely different. The first one works recursively and from the top to the bottom. The second one starts at 1 and works his way up. Actually, it is more iterative then recursive (it was probably written recursive to throw you off). It keeps the already calculated values instead of discarding them and therefore needs to invoke far less calculations.

Polygnome
  • 7,639
  • 2
  • 37
  • 57
5

The reason is two algorithms have different runtime complexities:

  • fib1 is in Ο(2n)
  • fib2 is in Ο(n)
Community
  • 1
  • 1
Gumbo
  • 643,351
  • 109
  • 780
  • 844
  • It's easier to visualize if you do a println at the begining of the function. Make sure to change n to lower value(10) before testing. – user845279 Jun 07 '12 at 17:19
3

Ultimately, it's because fib2 only uses tail end recursion. It only makes one recursive call at the end. Thus there isn't any "branching" associated with the recursion and leads to a linear time solution. The fact that it's a tail call also leads to certain compiler/VM optimizations where the recursion can be converted into an iterative procedure with lower overhead.

fib1 uses another recursive call in addition to the tail-call which causes the running time to be exponential.

Community
  • 1
  • 1
tskuzzy
  • 35,812
  • 14
  • 73
  • 140
  • while it _does_ use tail recursion, (1) That's not a major influence in the speed difference, and (2) I've heard Java doesn't do tail recursion. – Mooing Duck Jun 07 '12 at 17:41
  • Why should Java not do tail recusrions? Implementing an algorithm with tail recursion is absolutely possible in java. why shouldn't it? I#m not sure about how the JVM can optimize it, but is is possible nonetheless. – Polygnome Jun 07 '12 at 17:43
  • @MooingDuck: I'm not talking about the optimization where tail-calls turn into loops. I meant to emphasize the fact that there was only one recursive call. I edited my answer to make that more clear. – tskuzzy Jun 07 '12 at 17:46
  • 1
    @Polygnome: It was a long time ago, but I believe it doesn't do tail recursion because it complicates re-Jitting(if code changes), and stack traces. C/C++ don't do either of those, so tail recursion is easy for them. – Mooing Duck Jun 07 '12 at 17:49
  • http://stackoverflow.com/questions/3616483/why-does-the-jvm-still-not-support-tail-call-optimization – tskuzzy Jun 07 '12 at 17:52
  • @polygnome I think mooingduck is talking about tail recursion where the previous call is removed from the stack. I don't think java does that either. – user845279 Jun 07 '12 at 17:54
  • WHen you're talking about where java does/does not do tail recursion, it's a function of the JIT compiler itself, and not the language. Java does not do any unrolling at the bytecode level from what i understand. It's all done in the JIT compiler. This means that it's dependent on the VM itself whether or not tail recursion is optimized. – Matt Jun 07 '12 at 18:28
2

fib1 is an algorithm with O(2^n) runtime. fib2 is an algorithm with O(n) runtime.

The reason for this is pretty cool -- it's a technique called memoization. The work the program does is saved at each step, avoiding any extraneous calculation.

You can see it happen by unrolling the loop a couple more steps:

long fib2(int n) {
    return n == 0 ? 0 : fib2x(n, 0, 1);
}
long fib2x(int n, long p0, long p1) {
    return n == 1 ? p1 : fib2xy(n - 1, 1, 1);
}
long fib2xy(int n, long p0, long p1) {
    return n == 1 ? p1 : fib2xyz(n - 1, 1, 2);
}
long fib2xyz(int n, long p0, long p1) {
    return n == 1 ? p1 : fib2xyz(n - 1, p1, p0 + p1);
}

You can unroll this loop to any arbitrary number in the fibonacci sequence; each step builds on the calculation stored previously in the stack until n is depleted. This is in contrast to the first algorithm, which must redo this work at every step. Nifty!

E. B. Berg
  • 386
  • 1
  • 6
  • `fib2` is doing a little memoization that is in the code. Tail-end recursion is an optimizer thing, that may or may not be happening. – Mooing Duck Jun 07 '12 at 17:51