【问题标题】:Fibonacci using Recursion使用递归的斐波那契
【发布时间】:2019-01-21 16:44:15
【问题描述】:

这是我用最少的处理能力解决“斐波那契数列的第 n 项”的想法-

int fibo(int n, int a, int b){
    return (n>0) ? fibo(n-1, b, a+b) : a;
}

main(){
    printf("5th term of fibo is %d", fibo(5 - 1, 0, 1));
}

打印所有术语,直到第 n 个术语,

int fibo(int n, int a, int b){
    printf("%d ", a);
    return (n>0)? fibo(n-1, b, a+b): a;
}

我向我的大学教授展示了这段代码,据她说,这是解决斐波那契问题的错误方法,因为这没有抽象方法。我应该将函数称为 fibo(n) 而不是 fibo(n, 0, 1)。这对我来说不是一个满意的答案,所以我想请教 SOF 方面的专家。

与解决斐波那契问题的传统方法相比,它有自己的优势。我们使用两个并行递归来获得斐波那契的第 n 项 (fibo(n-1) + fibo(n-2)) 的技术可能会很慢,无法给出系列的第 100 项,而我的技术即使在最坏的情况下也会快得多场景。

为了抽象它,我可以使用默认参数,但 C 不是这样。虽然我可以使用类似 -

int fibo(int n){return fiboN(n - 1, 0, 1);}
int fiboN(int n, int a, int b){return (n>0)? fiboN(n-1, b, a+b) : a;}

但是将整个想法抽象出来就足够了吗?我应该如何说服其他人这种方法没有错(虽然有点模糊)?

(我知道,这不是我应该在 SOF 上问的问题,但我只是想在这里得到专家的建议。)

【问题讨论】:

  • 我很困惑。既然您没有将 a 和 b 用于任何事情,为什么它们会在那里?而且这不是在所有情况下都返回零吗?
  • 确实,正如@SamiKuhmonen 所说,此代码将始终返回 0。也许您的教授希望您解决这个问题而不是“抽象”问题?在我看来,“抽象”问题确实不是问题,而是更多功能,因为您可以为斐波那契计算器指定起始值。
  • 你测试过你的想法了吗?它有效吗? I don't think the 5th Fibonacci number is zero 但也许我错过了什么?
  • 第 5 个斐波那契数是第 4 个加第 3 个。所以试着计算第四个数字。那是第三个加上第二个。所以计算第三个......它是第二个加上第一个。我认为你走在正确的轨道上,坚持不止一个数字,但是当你调用fibo(5,0,1) 时传递的额外数字是第一个和第二个,在计算第 5 个时这些数字没有用。
  • 哦,我的错。我的代码中有一个错字。它应该返回 a 而不是 0。而且或多或少,它有效。请重新检查代码。

标签: c recursion fibonacci


【解决方案1】:

虽然@rici 的回答大多令人满意,但我只是想分享我在解决这个问题时学到的知识。所以这是我对使用递归查找斐波那契的理解-

  • 传统的实现fibo(n) { return (n < 2) n : fibo(n-1) + fibo(n-2);} 在时间和空间要求方面都非常低效。这不必要地构建堆栈。它需要 O(n) 堆栈空间和 O(rn) 时间,其中 r = (√5 + 1)/2。
  • 使用@Simion 的回答中建议的记忆技术,我们只需创建一个永久堆栈,而不是编译器在运行时创建的动态堆栈。因此内存需求保持不变,但时间复杂度以摊销的方式降低。但如果我们只需要使用一次它就没有帮助。
  • 我在问题中建议的方法需要 O(1) 空间和 O(n) 时间。在这里,也可以使用相同的记忆技术以摊销的方式减少时间要求。
  • 来自@rici 的帖子fib(2n) = fib(n)² + 2fib(n)fib(n - 1),因为他建议时间复杂度降低到 O(log n),我想,堆栈增长仍然是 O(n)。

所以我的结论是,如果我进行了适当的研究,时间复杂度和空间需求不能同时使用递归计算来降低。为了实现这两者,替代方案可以使用迭代,Matrix exponentiation or fast doubling

【讨论】:

    【解决方案2】:

    作为旁注,您几乎可以在没有递归的情况下执行fibonacci problem,使其成为我所知道的最快的方法。代码在java中:

    public int fibFor() {
        int sum = 0;
        int left = 0;
        int right = 1;
        for (int i = 2; i <= n; i++) {
            sum = left + right;
            left = right;
            right = sum;
        }
        return sum;
    }
    

    【讨论】:

    • 是的,我非常了解这种方法。由于我们学习的实际模块是递归,因此我们必须仅使用这个概念来实现所有内容。顺便说一句,谢谢您的建议。
    • 即使是阶乘的东西,当通过简单的循环结构实现时也非常快,消耗更少/几乎没有堆栈内存并且易于理解,但是大学再次需要我们用那些繁琐的递归方法来实现所有这些东西如果它们的效率低一百倍。
    【解决方案3】:

    了解递归中的基本情况应该是a 而不是0,在我看来这是一个极好的(尽管不是最优的)解决方案。该函数中的递归是尾递归,因此一个好的编译器将能够避免堆栈增长,从而使函数 O(1) 稳定且 O(n) 时间(忽略数字大小的快速增长)。

    你的教授是正确的,调用者不应该处理正确的初始化。所以你应该提供一个外部包装器来避免填写值的需要。

    int fibo(int n, int a, int b) {
        return n > 0 ? fibo(b, a + b) : a;
    }
    int fib(int n) { return fibo(n, 0, 1); }
    

    但是,提供和记录更通用的接口也很有用,以防调用者实际上想要改变初始值。

    顺便说一下,有一种更快的计算技术,基于递归

    fib(a + b - 1) = f(a)f(b) + f(a - 1)f(b - 1)
    

    b 替换为b + 1 会产生:

    fib(a + b) = f(a)f(b + 1) + f(a - 1)f(b)
    

    这些公式让我们一起计算:

    fib(2n - 1) = fib(n + n - 1)
                = fib(n)² + fib(n - 1)²
    
    fib(2n)     = fib(n + n)
                = fib(n)fib(n + 1) + fib(n - 1)fib(n)
                = fib(n)² + 2fib(n)fib(n - 1)
    

    这允许计算以 O(log n) 步执行,每个步产生两个连续的值。

    【讨论】:

    • 非常感谢您的指正。这正是我最初编写的代码,但我在发布问题时打错了字。但是如何计算特定解决方案的步数? IDK 这但可能根据我的一般理解,我的解决方案将完全采取 (n -1) 步骤。如果我错了,请纠正我。
    • @vivek: 自上次调用 n=0 以来,fibo 被调用 n+1 次
    • 但是由于我以 n 作为 n-1 的初始值调用它,它不会运行 n-1 次加上原始调用 ((n-1) + 1 = n) 吗?
    • @vivek:我不认为这是正确的; F(1)是1,不是0。F(0)是0。反正真的不重要;随着 n 变大,n+1 和 n 之间的差异逐渐变得不那么显着。
    【解决方案4】:

    使用递归并以处理能力最低为目标,解决fibonacci() 的方法是让每个调用返回 2 个值。也许一个通过返回值,另一个通过int * 参数。

    递归的通常想法是让一个顶级函数执行一次性准备和参数检查,然后是一个以精简方式编写的本地辅助函数。


    以下内容遵循 OP 的 int fibo(int n) 和助手 int fiboN(int n, additional parameters) 的想法

    递归深度是O(n),内存使用也是O(n)。

    static int fib1h(int n, int *previous) {
      if (n < 2) {
        *previous = n-1;
        return n;
      }
      int t;
      int sum = fib1h(n-1, &t);
      *previous = sum;
      return sum + t;
    }
    
    int fibo1(int n) {
      assert(n >= 0); // Handle negatives in some fashion
      int t;
      return fib1h(n, &t);
    }
    

    【讨论】:

      【解决方案5】:

      根据您的方法,您的结果将是 0。您只需进行递归,直到 n=0,然后返回 0。但是您还必须检查何时 n==1 并且您应该返回 1;你也有值 ab 并且你对它们什么都不做。

      我建议看看下面的递归函数,也许它会帮助你解决这个问题:

      int fibo(int n){
          if(n < 2){
              return n;
          }
          else 
          {
             return (fibo(n-1) + fibo(n-2));    
          }
      }
      

      这是研究递归的经典问题。

      EDIT1:根据@Ely 的建议,波纹管是一种优化的递归,具有记忆技术。当计算列表中的一个值时,它不会像第一个示例那样再次重新计算,但它会存储在数组中,并在需要时从该数组中取出:

      const int MAX_FIB_NUMBER = 10;
      
      int storeCalculatedValues[MAX_FIB_NUMBER] = {0};
      
      int fibo(int n){
      
          if(storeCalculatedValues[n] > 0)
          {
              return storeCalculatedValues[n];
          }
      
          if(n < 2){
              storeCalculatedValues[n] = n;
          }
          else 
          {
             storeCalculatedValues[n] = (fibo(n-1) + fibo(n-2));
          }
          return storeCalculatedValues[n];
      }
      

      【讨论】:

      • 效率太低了。
      • 我同意,但是是递归。
      • 不错的答案。我认为如果您可以在示例中包含记忆化技术会很有帮助,这样您也可以在答案中提供更有效的程序。
      • 我知道这种方法,我的教授正是教我们这种方法来解决问题。请看修改后的代码,有一处错别字。
      • 上述方法的问题是,它在内存中建立了两个并行堆栈来解决每个新的序列项。这样,要计算第 1000 项,需要几分钟(可能更快)才能得出解决方案。但在我建议的方法中,它会记住斐波那契的最后两项并给出第 n 项,而无需像传统方法那样每次都从头开始。
      【解决方案6】:

      以最少的处理能力解决“斐波那契数列的第 n 项”

      我可能不需要向你解释斐波那契数的递推关系。虽然你的教授给了你一个很好的暗示。

      抽象出细节。她是对的。如果您想要第 n 个斐波那契数,只需告诉程序即可:Fibonacci(n)

      由于您的目标是处理能力最低,因此您教授的提示也适用于称为 memoization 的技术,这基本上意味着 如果您计算了第 n 个斐波那契数,只需重复使用结果;无需重做计算。在文章中,您可以找到阶乘数的示例。

      为此,您可能需要考虑存储第 n 个斐波那契数的数据结构;如果该内存已经有一个斐波那契数,则只需检索它,否则将计算出的斐波那契数存储在其中。

      顺便说一句,在教学上没有帮助,但很有趣:还有一个 closed form expression 代表第 n 个斐波那契数。

      这对我来说不是一个令人满意的答案,所以我想问 SOF 方面的专家。

      “呃,你不认为你的教授是专家吗?”这是我的第一个想法。

      【讨论】:

      • 是的,她的回答让我不满意。抽象的东西是我在发布问题时添加的,她根本没有提及。请重新检查原始代码,我已经更正了错字。
      • “如果你计算了第 n 个斐波那契数一次,只需重复使用结果;无需重做计算”这正是我对其他解决方案的关注。如果我希望打印 100 条斐波那契数列,则需要在内部调用 fibo() 100 次 + 在内部调用它,如“return fibo(n-1) + fibo(n-2)”。对于记忆的事情,如果我遵循@simion 的回答中提到的一种技术,你认为创建一个全局数组是个好主意吗?
      猜你喜欢
      • 2010-12-03
      • 2014-04-02
      • 2016-07-01
      • 1970-01-01
      • 2017-11-18
      • 2014-01-08
      • 2011-12-14
      • 2012-11-29
      相关资源
      最近更新 更多