【问题标题】:Why does this recursive fibonacci function run so poorly?为什么这个递归斐波那契函数运行得这么差?
【发布时间】:2013-01-12 15:09:00
【问题描述】:

如果我运行以下代码:

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 毫秒。

我觉得我在这里做错了什么。我认为尾调用(递归斐波那契方法中的最后一行)将由编译器优化,使其更接近迭代版本的速度。有谁知道为什么运行如此缓慢?它只是一个写得不好的函数吗?

注意我正在使用 Oracle JDK 1.7

【问题讨论】:

  • 您的recursiveFib 不是尾递归的,因为+ 发生在递归调用之后。实际上它将是 O(2^n) (因为每个调用生成两个递归调用,这反过来又生成两个调用,依此类推)与迭代版本的 O(n) 相比。
  • @IanRoberts 看起来这可能是我的答案,伊恩!
  • 有 22 亿次函数调用,而迭代则需要 40~ 次迭代
  • @Esailija 这确实是这里的主要放缓

标签: java recursion tail-call-optimization


【解决方案1】:
return recFib(n - 1) + recFib(n - 2);

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

您可以查看this page,了解如何编写带有尾调用优化的递归斐波那契求解器。

【讨论】:

  • 我明白了。如何在 Java 中制作更优化的递归 fib 函数?
  • "既然你要进行两次递归调用..." 关键是 last 操作不是递归调用。最后一个操作是两个结果相加,所以不是尾递归。
【解决方案2】:

正如其他答案所述,您的函数不是尾递归的,这是斐波那契的尾递归版本:

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 不进行尾调用优化,因此将为每个递归调用分配一个堆栈帧,这使得这是一个相当昂贵的操作。然而,重要的是要注意这在技术上是依赖于实现的,请参阅 cmets 以获取指向执行 TCO 的 IBM SDK 的链接,以及this SO 问题以获取更多信息。

一个优化的版本是手动进行尾调用优化,将上面的代码转换为一个带有变量重新分配的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;
}

【讨论】:

  • IBM Java SDK 似乎做了尾调用优化(取自对一些旧帖子的评论,链接已更新):publib.boulder.ibm.com/infocenter/javasdk/v5r0/topic/…
  • @nhahtdh 很好,我更新了我的帖子以反映这一点,感谢发布链接!
  • 尾递归会自动隐含一个单独的方法吗?
【解决方案3】:

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

【讨论】:

  • 这对于 Java 来说不是一个很好的解释,因为它在 JVM 上运行,并且 JVM(理论上)可以进行尾部调用优化,这可能会使递归代码迭代地运行。是否实际完成取决于实现。
  • 即使理论上它可以进行任何类型的优化,但期望它会这样做是愚蠢到危险的。
【解决方案4】:

在递归代码中,调用次数与答案成正比,即 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 中的递归快得多。 (也有例外)

【讨论】:

    猜你喜欢
    • 2011-09-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-29
    • 2021-12-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多