Java 两种不同的递归方式

Java 两种不同的递归方式,java,algorithm,recursion,Java,Algorithm,Recursion,我用两种不同的方法计算fibonachi行。为什么fib1的执行时间比fib2长得多 public class RecursionTest { @Test public void fib1() { long t = System.currentTimeMillis(); long fib = fib1(47); System.out.println(fib + " Completed fib1 in:" + (System.

我用两种不同的方法计算fibonachi行。为什么fib1的执行时间比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);
    }
}
输出:

2971215073  Completed fib1 in:17414
2971215073  Completed fib2 in:0

原因是两种算法具有不同的运行时复杂性:

  • fib1
    处于
  • fib2
    位于Ⅹ(n)中

  • 因为两种算法的工作原理完全不同。让我用fib(5)来告诉你这个

    如果您调用fib1(5),它在内部调用fib1(4)和fib1(3),让我们用一个树来可视化:

    fib(5) / \ fib(4) fib(3) fib(5) / \ fib(4)fib(3) 现在,fib(4)在内部称为fib(3)和fib(2)

    现在我们有了这个:

    fib(5) / \ fib(4) fib(3) / \ / \ fib(3) fib(2) fib(2) fib(1) fib(5) / \ fib(4)fib(3) / \ / \ fib(3)fib(2)fib(2)fib(1) 我想现在已经很明显了,你应该能够填补剩下的部分

    编辑:您应该注意的另一件事是,它实际上必须多次执行相同的计算。在这幅图中,fib(2)和fib(3)都被多次调用。如果起始数字更大,情况会更糟。/edit

    现在,让我们来看看FiB2(5)。如果用0调用它,它将返回0。否则,它调用fib2x(n,0,1) 我们调用fib2x(5,0,1)。fib2x(n,0,1)现在在内部调用fib2x(n-1,p1,p0+p1)等等。 那么,让我们看看:

    fib2x(5, 0,1) => fib2x(4, 1,1) => fib2x(3, 1, 2) => fib2x(2, 2, 3) => fib2x(1, 3, 5) fib2x(5,0,1)=>fib2x(4,1,1)=>fib2x(3,1,2)=>fib2x(2,2,3)=>fib2x(1,3,5) 此时,它已达到返回条件并返回5

    所以,你的算法工作完全不同。第一种是从上到下递归工作的。
    第二个从1开始,一路向上。事实上,它比递归更具迭代性(编写递归的目的可能是为了摆脱您)。它保留已计算的值,而不是丢弃它们,因此需要调用的计算要少得多。

    fib1是一种运行时为O(2^n)的算法。fib2是一个具有O(n)运行时的算法

    这样做的原因很酷——这是一种叫做记忆的技术。程序所做的工作在每一步都被保存,避免了任何无关的计算

    通过将循环展开两个步骤,您可以看到它的发生:

    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);
    }
    

    您可以将此循环展开为斐波那契序列中的任意数字;每个步骤都建立在先前存储在堆栈中的计算基础上,直到n耗尽。这与第一个算法不同,第一个算法必须在每一步都重复这项工作。漂亮

    归根结底,这是因为
    fib2
    只使用。它只在最后进行一次递归调用。因此,并没有任何与递归相关的“分支”,并导致线性时间解决方案。事实上,它是一个尾部调用,也会导致某些编译器/虚拟机优化,其中递归可以转换为具有较低开销的迭代过程


    fib1
    除了尾部调用之外,还使用另一个递归调用,这会导致运行时间呈指数级增长。

    您应该使用调试器逐步完成这一过程,看看它是如何展开的,但是您的第一种技术调用1025119359方法调用,而您的第二种技术仅调用47。是的,这些算法完全不同。一个是运行时复杂度
    O(2^n)
    ,另一个是
    O(n)
    ,还有更快的算法:更严格地说,
    fib1()
    O(1.618^n)
    ,因为右分支是用n-2调用的。如果在函数开始时执行println,则更容易可视化。在测试之前,请确保将n更改为较低的值(10)。虽然它确实使用尾部递归,(1)这对速度差异没有重大影响,(2)我听说Java不执行尾部递归。为什么Java不应该执行尾部递归?用尾部递归实现算法在java中是绝对可能的。为什么不应该呢?我不确定JVM如何优化它,但这是可能的。@MooingDuck:我不是说尾部调用变成循环的优化。我想强调的是,只有一个递归调用。我编辑了我的答案以使其更清楚。@Polygenme:那是很久以前的事了,但我相信它不会进行尾部递归,因为它会使重新抖动(如果代码更改)和堆栈跟踪复杂化。C/C++都不做,所以尾部递归对他们来说很容易。
    fib2
    正在做一些代码中的记忆。尾端递归是一个优化器的事情,可能发生也可能不会发生。