【问题标题】:Recursion vs. Iteration (Fibonacci sequence)递归与迭代(斐波那契数列)
【发布时间】:2023-03-03 16:37:02
【问题描述】:

我有两种不同的方法,一种是使用迭代计算斐波那契数列到 nth 元素,另一种是使用递归方法做同样的事情。


程序示例如下所示:

import java.util.Scanner;

public class recursionVsIteration {

    public static void main(String[] args) {

        Scanner sc = new Scanner(System.in);

        //nth element input
        System.out.print("Enter the last element of Fibonacci sequence: ");
        int n = sc.nextInt();

        //Print out iteration method
        System.out.println("Fibonacci iteration:");
        long start = System.currentTimeMillis();
        System.out.printf("Fibonacci sequence(element at index %d) = %d \n", n, fibIteration(n));
        System.out.printf("Time: %d ms\n", System.currentTimeMillis() - start);

        //Print out recursive method
        System.out.println("Fibonacci recursion:");
        start = System.currentTimeMillis();
        System.out.printf("Fibonacci sequence(element at index %d) = %d \n", n, fibRecursion(n));
        System.out.printf("Time: %d ms\n", System.currentTimeMillis() - start);
    }

    //Iteration method
    static int fibIteration(int n) {
        int x = 0, y = 1, z = 1;
        for (int i = 0; i < n; i++) {
            x = y;
            y = z;
            z = x + y;
        }
        return x;
    }

    //Recursive method
    static int fibRecursion(int  n) {
        if ((n == 1) || (n == 0)) {
            return n;
        }
        return fibRecursion(n - 1) + fibRecursion(n - 2);
    }
}

我试图找出哪种方法更快。我得出的结论是,数字越少,递归越快,但是随着 nth 元素值的增加,递归变得更慢,迭代变得更快。以下是三种不同 n 的三种不同结果:


示例 #1(n = 10)

Enter the last element of Fibonacci sequence: 10
Fibonacci iteration:
Fibonacci sequence(element at index 10) = 55 
Time: 5 ms
Fibonacci recursion:
Fibonacci sequence(element at index 10) = 55 
Time: 0 ms

示例 #2(n = 20)

Enter the last element of Fibonacci sequence: 20
Fibonacci iteration:
Fibonacci sequence(element at index 20) = 6765 
Time: 4 ms
Fibonacci recursion:
Fibonacci sequence(element at index 20) = 6765 
Time: 2 ms

示例 #3(n = 30)

Enter the last element of Fibonacci sequence: 30
Fibonacci iteration:
Fibonacci sequence(element at index 30) = 832040
Time: 4 ms
Fibonacci recursion:
Fibonacci sequence(element at index 30) = 832040
Time: 15 ms

我真正想知道的是为什么突然迭代变得更快而递归变得更慢。如果我错过了这个问题的一些明显答案,我很抱歉,但我还是编程新手,我真的不明白背后发生了什么,我想知道。请提供一个很好的解释或指出正确的方向,以便我自己找出答案。另外,如果这不是测试哪种方法更快的好方法,请告诉我并建议我不同的方法。

提前致谢!

【问题讨论】:

  • 递归调用函数会增加开销。
  • 这两种方法完全相同(甚至从图片中排除递归/迭代)
  • 第一个问题:您的基准测试方法存在很大缺陷。您确实没有做足够的工作来准确测量差异。您应该使用System.nanoTime,并重复调用几次,以便测量有用的工作量。接下来,查看每个调用的复杂性...计算出每种情况下完成了多少工作,随着 n 的增长。提示:尝试在纸上说明如果调用 fibRecursion(8) 与 fibIteration(8) 会发生什么。
  • 最快的方法是closed form
  • 斐波那契数列的迭代、递归和记忆实现的一个很好的比较here

标签: java recursion iteration


【解决方案1】:

为简洁起见,令 F(x) 为递归斐波那契

F(10) = F(9)                      + F(8)
F(10) = F(8)        + F(7)        + F(7) + F(6)
F(10) = F(7) + F(6) + F(6) + F(5) + 4 more calls.
....

所以你调用 F(8) 两次, F(7) 3 次,F(6) 5 次,F(5) 7 次..等等

所以随着输入的增加,树会变得越来越大。

【讨论】:

  • 好吧,有道理,你能不能告诉我为什么迭代变快了?
  • 很简单,对于Fib(N),循环会执行N次。但是到了我上面所做的第三级递归,我们已经在 N=10 的情况下调用了该函数 14 次,所以即使循环迭代的成本与函数调用完全相同,我们希望循环执行得更好在 N = 10 时(我们仍然需要考虑循环设置,这可能是因为小 N 的递归稍快,但这可能是由于您上面提到的@JonSkeet 的计时方法)
  • 感谢大家的回复,我想我现在明白了大意,我最喜欢你的回答,谢谢先生!
  • 速度不同的原因是:调用函数就是把当前函数值和调用参数入栈,函数调用完成后返回值入栈,最后一次调用函数是通过从堆栈中加载其状态来加载的,并且也会读取返回的值来处理它。这些存储和加载是内存访问,总是比 CPU 寄存器内部的计算慢。特别是如果读取来自外部 CPU 缓存 L1。比较求和和缓存读取的 CPU 周期:读取需要 100 多个周期。
  • @visorz 虽然您所说的在某种意义上是正确的,但在这种特定情况下实际上是不正确的。快速增长的是一遍又一遍对相同值的递归,而不是函数调用的开销。
【解决方案2】:

This article 对递归和迭代进行了比较,并涵盖了它们在生成斐波那契数方面的应用。

如文章所述,

性能不佳的原因是每次递归调用的病态级别的寄存器的大量推送。

这基本上是说递归方法的开销更大。

另外,看看Memoization

【讨论】:

    【解决方案3】:

    在执行斐波那契算法的递归实现时,您会通过一遍又一遍地重新计算相同的值来添加冗余调用。

    fib(5) = fib(4) + fib(3)
    fib(4) = fib(3) + fib(2)
    fib(3) = fib(2) + fib(1)
    

    请注意,fib(2) 将针对fib(4)fib(3) 进行冗余计算。 然而,这可以通过一种称为Memoization 的技术来克服,该技术通过存储您计算过的值来提高递归斐波那契的效率。对已知值的进一步调用 fib(x) 可以由简单的查找代替,从而无需进一步递归调用。

    这是迭代和递归方法的主要区别,如果您有兴趣,还有其他更多的efficient algorithms 计算斐波那契数。

    【讨论】:

      【解决方案4】:

      为什么递归变慢了?

      当您再次调用您的函数本身(作为递归)时,编译器会为该新函数分配新的激活记录(只需将其视为普通堆栈)。该堆栈用于保存您的状态、变量和地址。编译器为每个函数创建一个堆栈,这个创建过程一直持续到达到基本情况。所以,当数据量变大时,编译器需要很大的栈段来计算整个过程。在此过程中也会计算和管理这些记录。

      此外,在递归中,堆栈段被在运行时引发。编译器不知道编译期间会占用多少内存

      这就是为什么如果你没有正确处理你的基本情况,你会得到StackOverflow异常:)。

      【讨论】:

        【解决方案5】:

        按照您的方式使用递归,时间复杂度为O(fib(n)),非常昂贵。迭代方法是O(n) 这没有显示,因为 a) 你的测试很短,代码甚至不会被编译 b) 你使用的数字非常小。

        这两个示例运行得越多,速度就会越快。一旦一个循环或方法被调用了 10,000 次,它就应该被编译为本机代码。

        【讨论】:

        • 什么是 O(fib(n))?是否等于 O(2^n)?
        • @ShahidMZubair 它等价于O(((1 + sqrt(5))/2) ^n),它总是小于O(2^n)
        【解决方案6】:

        如果有人对带有数组的迭代函数感兴趣:

        public static void fibonacci(int y)
        {
            int[] a = new int[y+1];
            a[0] = 0;
            a[1] = 1;
            System.out.println("Step 0: 0");
            System.out.println("Step 1: 1");
            for(int i=2; i<=y; i++){
                a[i] = a[i-1] + a[i-2];
                System.out.println("Step "+i+": "+a[i]);
            }
            System.out.println("Array size --> "+a.length);
        }
        

        此解决方案因输入值 0 而崩溃。

        原因:数组a会被初始化0+1=1,但是a[1]的连续赋值会导致索引越界异常。

        要么在y=0 上添加一个返回0 的if 语句,要么通过y+2 初始化数组,这将浪费1 int 但仍然是恒定空间并且不会改变大O

        【讨论】:

          【解决方案7】:

          我更喜欢使用黄金数字的数学解决方案。享受

          private static final double GOLDEN_NUMBER = 1.618d;
          
          public long fibonacci(int n) {
              double sqrt = Math.sqrt(5);
          
              double result = Math.pow(GOLDEN_NUMBER, n);
          
              result = result - Math.pow(1d - GOLDEN_NUMBER, n);
          
              result = Math.round(result / sqrt);
          
              return Double.valueOf(result).longValue();
          }
          

          【讨论】:

            【解决方案8】:

            当您在寻找完成特定算法所花费的时间时,最好总是考虑时间复杂度。

            用 O(something) 来评估论文的时间复杂度。

            比较上述两种方法,迭代方法的时间复杂度是O(n),而递归方法的时间复杂度是O(2^n)。

            我们试着求fib(4)的时间复杂度

            迭代法,循环计算4次,所以它的时间复杂度是O(n)

            递归方法,

                                           fib(4)
            
                         fib(3)              +               fib(2)
            
                  fib(2)   +    fib(1)           fib(1)     +       fib(0)
            
            fib(1)  +  fib(0)
            

            所以 fib() 被调用了 9 次,当 n 的值很大时,它比 2^n 略低,即使很小(记住 BigOh(O) 负责 upper bound)。

            因此,我们可以说迭代方法在polynomial time 中进行评估,而递归方法在exponential time 中进行评估

            【讨论】:

              【解决方案9】:

              您使用的递归方法效率不高。我建议你使用尾递归。与您的方法相比,尾递归在任何时间点都只在堆栈中保留一个函数调用。

              public static int tailFib(int n) {
                  if (n <= 1) {
                      return n;
                  }
                  return tailFib(0, 1, n);
              }
              
              private static int tailFib(int a, int b, int count) {
                  if(count <= 0) {
                      return a;
                  }
                  return tailFib(b, a+b, count-1);
              }
              
              public static void main(String[] args)  throws Exception{
                  for (int i = 0; i <10; i++){
                      System.out.println(tailFib(i));
                  }
              }
              

              【讨论】:

                【解决方案10】:

                我有一个递归解决方案,您可以将计算值存储在其中以避免进一步不必要的计算。代码如下,

                public static int fibonacci(int n) {
                
                        if(n <=  0) return 0;
                        if(n == 1) return 1;
                
                        int[] arr = new int[n+1];
                
                        // this is faster than using Array
                        // List<Integer> lis = new ArrayList<>(Collections.nCopies(n+1, 0));
                
                        arr[0] = 0;
                        arr[1] = 1; 
                
                        return fiboHelper(n, arr);
                    }
                
                    public static int fiboHelper(int n, int[] arr){
                
                        if(n <= 0) {
                            return arr[0];
                        }
                
                        else if(n == 1) {
                            return arr[1];
                        }
                
                        else {
                
                            if( arr[n-1] != 0 && (arr[n-2] != 0 || (arr[n-2] == 0 && n-2 == 0))){    
                                return arr[n] = arr[n-1] + arr[n-2]; 
                            }
                
                            else if (arr[n-1] == 0 && arr[n-2] != 0 ){
                                return arr[n] = fiboHelper(n-1, arr) + arr[n-2]; 
                            }
                
                            else {
                                return  arr[n] = fiboHelper(n-2, arr) + fiboHelper(n-1, arr );
                            } 
                
                        }             
                    }
                

                【讨论】:

                • 如果您需要更多解释,我在这里为您提供帮助。
                猜你喜欢
                • 2018-08-01
                • 2011-07-27
                • 2012-11-19
                • 2016-11-07
                • 2012-02-16
                • 1970-01-01
                • 2010-12-03
                相关资源
                最近更新 更多