为什么这个递归的斐波纳契函数运行得如此糟糕?

时间:2013-01-12 15:09:01

标签: java recursion tail-call-optimization

如果我运行以下代码:

public static void main(String[] argsv) {

    long whichFib = 45;
    long st;
    st = System.currentTimeMillis();
    System.out.println(recursiveFib(whichFib));
    System.out.println("Recursive version took " + (System.currentTimeMillis() - st) + " milliseconds.");

    st = System.currentTimeMillis();
    System.out.println(iterativeFib(whichFib));
    System.out.println("Iterative version took " + (System.currentTimeMillis() - st) + " milliseconds.");

}

public static long recursiveFib(long n) {

    if (n == 0)
        return 0;
    if (n == 1 || n == 2)
        return 1;

    return recFib(n - 1) + recFib(n - 2);
}

public static long iterativeFib(long n) {

    if (n == 0)
        return 0;
    else if (n == 1 || n == 2)
        return 1;

    long sum = 1;
    long p = 1;
    long u = 1;

    for (int i = 2; i < n; i++) {
        sum = p + u;
        p = u;
        u = sum;
    }

    return sum;
}

我得到以下输出:

  

1134903170   递归版本耗时5803毫秒。   1134903170   迭代版本需要0毫秒。

我觉得我在这里做错了。我认为尾部调用(递归fibonacci方法中的最后一行)将由编译器优化,使其更接近迭代版本。有没有人有任何想法为什么这么慢?它只是一个写得不好的函数吗?

N.B。我正在使用Oracle JDK 1.7

4 个答案:

答案 0 :(得分:6)

return recFib(n - 1) + recFib(n - 2);

由于您正在进行两次递归调用,而不是一次,因此编译器不太可能进行传统的尾调用优化。

您可以查看this page有关如何使用尾调用优化编写递归Fibonacci求解器的想法。

答案 1 :(得分:5)

正如其他答案所指出的那样,你的函数不是尾递归的,这里是斐波那契的尾递归版本:

long fibonacci(int n) {
    if (n == 0)
        return 0;
    else
        return fibonacciTail(n, 1, 0, 1);
}

long fibonacciTail(int n, int m, long fibPrev, long fibCurrent) {
    if (n == m)
        return fibCurrent;
    else
        return fibonacciTail(n, m + 1, fibCurrent, fibPrev + fibCurrent);
}

此外,JVM不进行尾调用优化,因此将为每个递归调用分配堆栈帧,这使得这非常昂贵。但是,请务必注意这在技术上依赖于实现,请参阅有关执行TCO的IBM SDK链接的注释,以及this所以请提供更多信息。

优化版本将手动进行尾调用优化,将上述内容转换为带有可变重新分配的while循环:

long fibonacciIter(int n) {
    int m = 1;
    long fibPrev = 0;
    long fibCurrent = 1;
    while (n != m) {
        m = m + 1;
        int current = fibCurrent;
        fibCurrent = fibPrev + fibCurrent;
        fibPrev = current;
    }
    return fibCurrent;
}

答案 2 :(得分:1)

在递归版本中,您正在递归地创建函数,这很昂贵,因为函数调用涉及将变量推入堆栈,堆栈管理等,而迭代操作在同一堆栈上。

答案 3 :(得分:1)

在递归代码中,调用次数与答案成正比,即它是O(exp(n))

在迭代方法中,运行时间与循环次数成比例。 O(n)

更糟糕的是,循环操作比递归调用快得多,因此即使相同迭代的顺序仍然会明显加快。

你可以像这样编写迭代的fib。

public static long iterativeFib(int n) { // no chance of taking a long
    long a = 0, b = 1;    
    while(n-- > 0) {
        long c = a + b;
        a = b;
        b = c;
    }
    return c;
}
  

有没有人知道为什么这么慢?它只是一个写得不好的函数吗?

Java不是一种函数式语言,它不进行尾调用优化。这意味着迭代通常比Java中的递归快得多。 (也有例外)