我是一个初学者程序员,我遇到了这个问题,那就是在斐波那契数列中找到第n个数字。

我以前使用for循环解决了这个问题;今天,我了解了递归,但是有一个问题:当我将40或41传递给递归函数时,计算它会花费一些时间,而在迭代方法中,它将立即为我提供答案。

我有以下问题:


为什么大多数人(在Internet上)建议使用递归?因为编写程序更简单? (逻辑上,我认为我们应该以一种快速,简单的方式编写它)。
这是像我这样的初学者目前可以处理的两种方法。有没有比这两种方法更好的方法?这些方法复杂吗?

这是递归方法:

    #include <iostream>
    using namespace std;

    unsigned long long fibonacci(unsigned long long n);

    unsigned long long fibonacci(unsigned long long n){
        if(n<=1)
            return n;                                //base case
        return fibonacci(n-1) + fibonacci(n-2);      //recursive case
    }

    int main(){
        unsigned int a {};
        cout << "Enter number: ";
        cin >> a;
        cout << fibonacci(a) << endl;
        return 0;
    }


这是迭代(循环)方法:

#include <iostream>
using namespace std;
int main() {
    int n{};
    unsigned long long t1{0}, t2{1};
    unsigned long long sum{};
    cout << "Enter the number of terms: ";
    cin >> n;
    cout << "Fibonacci Series: ";
    for (int i{2}; i < n; ++i) {
        sum = t1 + t2;
        t1 = t2;
        t2 = sum;
    }
    cout << t2;
    return 0;
}


注意:我知道using namespace std是一个坏主意,但我也尝试了没有名称空间的相同操作,但仍然会出现延迟,所以我在这里这样做是因为很容易理解。


Edit1:首先,我要感谢所有评论并回答我的问题的人……说实话,我认为这个问题不会带来很多对社区的关注,所以我感谢您投入的时间,这对我来说意义重大。 br />
问:当您说有比这两种方法更好的方法时,您说的更好是什么?是时候执行或执行计算了?

问:当您开始工作时,您是什么意思y大多数人(在Internet上)都使用递归?
答:我已经看到了使用递归解决问题的代码。我见过的两个常见问题是斐波那契数和阶乘数。对于阶乘,我也尝试了两种方法(迭代和递归),但是当我键入40或50之类的大数时,这两种方法都没有延迟。我在递归上看到的常见问题是最终您可能会运行内存不足,因为它多次调用自身,这会导致堆栈溢出(因为堆栈将填充一部分内存)。尽管在此示例中不会发生此问题。所以这在这里提出了另一个问题:

我们什么时候应该使用递归?

评论

对于该站点上的大多数审阅者而言,如果您确实具有using名称空间std,则更容易理解。声明。

using名称空间std;与速度无关。它与可维护性有关。从长远来看,这将是一个坏习惯,它将使您的代码难以维护(并导致bug(请参阅链接的文章)。这就是为什么这被认为是不好的做法。为什么“使用命名空间std”被认为是不好的做法? />
“迭代或递归”-都不是,有封闭形式的解决方案

@ canton7:从Fib(70)开始,封闭形式的解决方案将是错误的,假设使用了binary64 double。迭代解决方案将使用不超过Fib(92)的64位整数算术给出正确答案。

我完全看不到为什么递归方法的答案很慢的解释:原因是它通过将许多加在一起构成了斐波那契数,从而建立了斐波那契数(因此它们的数量将呈指数增长)。

#1 楼


为什么大多数人(在Internet上)建议使用递归,因为它更容易编写程序?从逻辑上讲,我认为我们应该以一种快速而简单的方式编写它。


这是一个可以理解的问题。我在2004年写了一篇有关该主题的文章,您可以在这里阅读:

https://docs.microsoft.com/en-us/archive/blogs/ericlippert/how-not-to -teach-recursion

总结:有两个很好的理由教人们使用递归来解决Fib:


首先,因为它清楚地说明了你今天学到了。单纯将递归定义转换为递归函数通常会导致性能下降。对于初学者来说,这是一个重要的教训。 (练习:对于给定的n,您的幼稚递归程序执行了几次加法?答案可能会让您感到惊讶。)
第二,因为第一课使我们有机会带领初学者学习如何编写递归

不幸的是,正如您所发现的那样,互联网上的许多人还没有内化过通过fib进行递归教学仅是对错误使用递归以及如何进行递归的说明的有用方法。

如果人们尝试教递归通过提供好坏的递归算法来做到这一点,那就更好了。教了如何发现并避免不良情况。


还有比这两种方法更好的方法吗?


您很快就会学到的另一课是,问“哪个更好?”是一种肯定的方式来获得答复:“您能描述一个明确的指标以改善生活吗?”

因此:您能描述一个明确的指标以改善生活吗?我们的目标是打印出第n个fib号,您可以采用比任何一种解决方案都“更好”的方法:

做完了只能容纳这么多fib号码。您可以在Internet上查找它们,将它们复制到您的程序中,并且您有一个简短,快速的fib程序,根本没有循环。这对我来说是“更好”。 />
这些方法是否复杂?进入循环”。

这是一个见解的问题。让我这样说吧。

我在编译器团队工作,并采访了很多人。我的标准编码问题涉及在二叉树上编写一种简单的递归算法,该算法在以幼稚的方式编写时效率不高,但是可以通过进行一些简单的重构来使其高效。如果应聘者无法编写清晰,直接,有效的代码,那将是一件容易的事。

有趣的地方是当我问“假设您必须从遍历这棵树;您怎么做?”

有消除递归的标准技术。动态编程减少了递归。您可以创建一个显式堆栈并使用循环。您可以使整个算法尾部递归,并使用支持尾部调用的语言。您可以在共同通话中使用一种语言。您可以使用连续传递样式并构建“蹦床”执行循环。

从新手的角度来看,其中一些技巧非常复杂。我问这个问题是因为我想知道开发者工具箱中的内容。

评论


\ $ \ begingroup \ $
我也喜欢将其作为面试问题。不是因为有代码(因为每个人都可以编写递归版本,而任何看过任何编码测试的人都可以看到迭代版本)。但是因为它导致了很多关于技术的讨论。喜欢这篇文章。
\ $ \ endgroup \ $
–马丁·约克
20-2-19在6:38

\ $ \ begingroup \ $
“这些方法是否复杂”-斐波那契特定于迭代的算法与优化递归无关,一种涉及将矩阵求幂,另一种涉及Lucas序列。我认为这是OP所要问的。
\ $ \ endgroup \ $
–rcgldr
20-2-19在8:59

\ $ \ begingroup \ $
@DavidPeterson:没错;在许多操作系统(尤其是Windows)中,您仅获得固定数量的堆栈空间,并且默认情况下,方法调用的激活帧位于堆栈上。这意味着无限或非常深的递归将“破坏堆栈”。了解我讨论的用于删除递归的技术的一个原因是为了防止堆栈溢出。
\ $ \ endgroup \ $
–埃里克·利珀特
20-2-19在20:02

\ $ \ begingroup \ $
@DavidPeterson:您可能要研究的概念是延续性。一段特定代码的延续是完成该代码后将发生的事情。堆栈是我们如何在“常规”函数调用中确定延续性的方法:您调用一个函数,接下来会发生什么?函数抛出异常,在这种情况下,延续是异常处理程序,或者它从不正常返回,或者它确实返回了正常; “正常返回”继续信息存储在堆栈中。
\ $ \ endgroup \ $
–埃里克·利珀特
20-2-19在20:04



\ $ \ begingroup \ $
@DavidPeterson:由于异步调用的激活在逻辑上并没有形成堆栈,因此我们不使用堆栈来存储异步工作流的延续-或更确切地说,我们使用较少。绕过连续性是很棘手的,但是一旦完成,关于编程的许多事实就会变得更加清晰。
\ $ \ endgroup \ $
–埃里克·利珀特
20-2-19在20:05

#2 楼

如其他答案所述,您的迭代算法要优于递归算法,因为前者会记住先前的中间结果(或至少一个这样的结果),而后者则不会。当然,可以编写可记住先前结果的递归算法。

对于斐波那契数,这很简单,因为您只需要记住一个这样的结果。为了便于阅读,请使用C ++ 17的元组拆包语法: 。足够聪明的编译器可以优化成对打包和解包的开销,以使函数调用开销(请参阅编译器资源管理器)成为递归算法相对于迭代算法的唯一缺点。 br />
我将函数声明括在自己的命名空间detail::fibonacci_impl和子命名空间fibonacci(n)中。 )放入一个单独的命名空间。对于要在自己的程序之外使用的声明(例如,编程库),强烈建议这样做,以避免名称遮盖问题和一般的混淆。

如果您的库声明的内容是通常只打算从该库内部使用它,通常将其放在一个子命名空间中,该子命名空间的名称指示其预期的性质。我遇到的有关此类子命名空间名称的示例包括:fibonacci(n-1)pairmynamespace或更短的mynamespace::detail

评论


\ $ \ begingroup \ $
不要定义以两个下划线开头的名称。
\ $ \ endgroup \ $
–罗兰·伊利格(Roland Illig)
20-2-20在1:54

\ $ \ begingroup \ $
一个下划线不如两个下划线一样糟糕,但是仍然很不正常。我不记得单个下划线的确切规则,但是其中一些名称仍然保留。
\ $ \ endgroup \ $
–罗兰·伊利格(Roland Illig)
20-2-21在14:48

\ $ \ begingroup \ $
为什么要在函数名称的开头加上下划线?
\ $ \ endgroup \ $
–theonlygusti
20-2-21在19:18

\ $ \ begingroup \ $
@RolandIllig:确认并修复。我阅读了C ++命名样式,这些样式建议使用(匿名)子命名空间来声明与实现细节有关的声明。
\ $ \ endgroup \ $
–大卫·福斯特(David Foerster)
20-2-22在20:48

\ $ \ begingroup \ $
@theonlygusti:见上文
\ $ \ endgroup \ $
–大卫·福斯特(David Foerster)
20-2-22在20:48

#3 楼

在递归版本的代码中,您不需要功能原型unsigned long long fibonacci(unsigned long long n);

您提到的代码中不应包含using namespace std;语句。

我们可以不能回答


为什么大多数人(在互联网上)建议使用递归,因为它更容易编写程序?(从逻辑上讲,我认为我们应该以某种方式编写它快速而简单)


,因为它是一种意见。

在迭代版本中,您还应该具有fibonacci(unsigned long long n)函数,以简化main()。 />

评论


\ $ \ begingroup \ $
谢谢您的帮助。也可以说,如果我们在代码中不包括使用命名空间std,那么对于每个cout,我们都应该说std :: cout ...等等元素...否则我们会收到一个错误...
\ $ \ endgroup \ $
–大卫·彼得森(David Peterson)
20-2-18在19:43

\ $ \ begingroup \ $
@DavidPeterson是的,并且std :: cout比cout更清晰,因为后者可以表示std :: cout或my :: obscure :: library :: cout(这会使<<操作符重载以在其中打印值鼻恶魔的形式),具体取决于上下文和ADL,因此代码阅读器必须停下来思考一会儿。这个问题在cout中不是很明显,但是要考虑数据,arg,访问等,我无法立即意识到它们实际上不是标准库中的名称。使用命名空间std;还会用大量的通用标识符污染全局名称空间,从而导致名称冲突。
\ $ \ endgroup \ $
– L. F.
20-2-19在4:02

#4 楼



David Foerster的__fibonacci_impl既具有矩阵表示形式,可以将矩阵变成对角线形状,从而求出两个指数函数之差,其中后者的绝对值小于



 const double sqr5 = sqrt(5);
 const double phi = 0.5 * (sqr5+1);

 double Fn = floor( pow(phi,n) / sqr5 + 0.5); // n<=70


评论


\ $ \ begingroup \ $
从Fib(71)开始失败。
\ $ \ endgroup \ $
–总统James K. Polk
20-2-20在1:05

\ $ \ begingroup \ $
@MonicaPolk由于数值误差为两倍,因此精度有限。基于整数基于int64的较慢方法将在Fib(92)周围造成整数溢出,从而导致失败而不仅仅是错误。
\ $ \ endgroup \ $
– ALX23z
20-2-20在6:30

\ $ \ begingroup \ $
@ ALX23z-对于无符号64位整数,限制为Fib(93)。
\ $ \ endgroup \ $
–rcgldr
20-2-21在8:21

\ $ \ begingroup \ $
我只是希望这是个玩笑,因为这几乎回答了OP的所有问题,但完全没有讲到重点。
\ $ \ endgroup \ $
– LittleEwok
20-2-22在12:36

\ $ \ begingroup \ $
@ ALX23z:然后,根据您自己的分析,整数形式在苹果对苹果的比较中是优越的。解释从Fib(71)开始的此方法的失败如何比在Fib(93)处的整数迭代的失败更可悲?
\ $ \ endgroup \ $
–总统James K. Polk
20 Feb 24'0:58

#5 楼


有没有比这两种方法更好的方法?这些方法是否复杂?


有更好的方法,尽管不是那么复杂,但很少有人能够开发这样的方法(例如Lucas序列关系)可以自己使用,而无需依赖某些参考。对于问题中所示的递归版本,对fibonacci(n)进行实例(调用)的次数将2 * fibonacci(n + 1)-1.

对于更好的方法,可以通过提高2 x 2矩阵= {{ 1,1},{1,0}}乘幂通过重复平方得到幂,但这需要12个变量。使用基于Lucas序列关系的方法,可以将其减少为5个变量。

示例代码; c和d用于重复平方,而a和b是累加结果,最终为a = fib(n + 1),b = fib(n)。

注意:较早的编译器可能缺少<inttypes.h><stdint.h>。如果不存在<inttypes.h>(Visual Studio 2010),请为uint64_t使用编译器特定的格式字符串。如果不存在<stdint.h>(Visual Studio 2005),请使用typedef ... uint64_t(通常是unsigned long long)和适当的格式字符串。

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

uint64_t fib(uint64_t n)
{
    uint64_t a, b, c, d;
    a = d = 1;
    b = c = 0;
    while (1) {
        if (n & 1) {
            uint64_t ad = a*d;
            a = ad + a*c + b*d;
            b = ad + b*c;
        }
        n >>= 1;
        if (n == 0)
            break;
        {
            uint64_t dd = d*d;
            d = dd + 2 * d*c;
            c = dd + c*c;
        }
    }
    return b;
}

int main(void)
{
    uint64_t n;
    for (n = 0; n <= 93; n++)
        printf("%20" PRIu64 " %20" PRIu64 "\n", n, fib(n));
    return 0;
}



代码基于Lucas斐波那契数的序列关系。

https://en.wikipedia.org/wiki/Lucas_sequence#Other_relations

具体来说,这些等式:

初始状态:

F(m)   = F(m-1) + F(m-2)
F(m+n) = F(m+1) F(n) + F(m) F(n-1)
F(2n)  = F(n) L(n) = F(n) (F(n+1) + F(n-1))
       = F(n)((F(n) + F(n-1)) + F(n-1))
       = F(n) F(n) + 2 F(n) F(n-1)


n被视为2的幂的和:2 ^ a + 2 ^ b + ...
对于每个迭代i(从0开始),让p = 2 ^ i,然后

a = F(1) = 1
b = F(0) = 0
c = F(0) = 0
d = F(1) = 1


要前进到下一个迭代,c和d是前进到F(n的2的次幂):

c = F(p-1)
d = F(p)


在计算a和b时,令m = n的当前累积位数之和:

d' = F(2p) = F(p) F(p+1) + F(p) F(p-1)
   = F(p)(F(p) + F(p-1)) + F(p) F(p-1)
   = F(p) F(p) + F(p) F(p-1) + F(p) F(p-1)
   = F(p) F(p) + 2 F(p) F(p-1)
   = d d + 2 c d

c' = F(2p-1) = F(p+p-1) = F(p+1) F(p-1) + F(p) F(p-2)
   = (F(p) + F(p-1)) F(p-1) + F(p) (F(p) - F(p-1))
   = F(p) F(p-1) + F(p-1) F(p-1) + F(p) F(p) - F(p) F(p-1)
   = F(p) F(p) + F(p-1) F(p-1)
   = d d + c c


将n中的a和b更新为1,对应于p = 2的当前功率:

b = F(m)
a = F(m+1)



请注意,如果b'是uint64_t的最大值,则a'将溢出,但这不是问题。但是,可以对算法进行修改,以使完成后a = fib(n-1):

a' = F(m+1+p) = F(m+2) F(p) + F(m+1) F(p-1)
   = (F(m+1)+F(m)) F(p) + F(m+1) F(p-1)
   = F(m+1) F(p) + F(m) F(p) + F(m) F(p-1)
   = a d + b d + b c

b' = F(m+p) = F(m+1) F(p) + F(m) F(p-1)
   = a d + b c


评论


\ $ \ begingroup \ $
#define使得此程序不必要地复杂。加上错误的变量名。而且main缺少返回类型。我们已经不在1980年代了。
\ $ \ endgroup \ $
–罗兰·伊利格(Roland Illig)
20-2-20在1:52

\ $ \ begingroup \ $
@RolandIllig-变量名与Lucas序列方法的其他示例中使用的变量名相同,但c和d有时也分别命名为p和q。我不知道main上的int如何丢失,但现在已修复。我一直在试图找到该代码的当前示例时遇到问题,这些示例清楚地表明这是Lucas序列的变体。如果/找到链接,我将使用链接更新我的答案。如果编译器没有将两个产品优化为单个变量(寄存器),则定义可能是一个遗留问题。这是一个旧算法。
\ $ \ endgroup \ $
–rcgldr
20 Feb 20'2:35



\ $ \ begingroup \ $
@RolandIllig-我在这里找到了使用a,b,p,q的方法的一种变体,但是没有引用Lucas序列。
\ $ \ endgroup \ $
–rcgldr
20 Feb 20'2:50



\ $ \ begingroup \ $
@RolandIllig-我找不到该代码的工作链接,因此我根据卢卡斯序列关系用自己的解释更新了答案。
\ $ \ endgroup \ $
–rcgldr
20-2-20在11:03



\ $ \ begingroup \ $
@RolandIllig-定义已消失,由块局部变量代替。 Toby Speight更新了printf以使用inttypes.h。 (请注意,VS2010没有inttypes.h,VS2005没有stdint.h,我在回答中提到过)。
\ $ \ endgroup \ $
–rcgldr
20-2-20在19:22

#6 楼

通过使用该迭代解决方案,您可以间接使用动态编程(DP)

问题1的答案:

在某些情况下递归可能会更快。

例如,假设您的二维道路尺寸为n * m。道路上有障碍物,所以您不能通过它们。

目的是检查是否存在从左上角到右下角的任何路径(您只能移动右或向下)。

递归解将获胜,因为在最佳和最差情况下迭代解决方案将分别采用O(N * M),但在最佳情况下将采用O(N + M),而在最坏情况下将采用O(N * M)

这里给出了一个具有详细解释的迭代解决方案,但是我找不到递归解决方案的任何来源。

问题2的答案:

您的递归解决方案比迭代解决方案要慢得多,因为您没有使用备忘录。

记忆起来并不难理解。

请尝试访问此链接:https://www.quora.com/How-should-I-explain-dynamic-programming-to-a-4岁的孩子

评论


\ $ \ begingroup \ $
我不明白为什么两个人不赞成我的答案。请问有人解释为什么,这样以后我就不会再犯同样的错误了?非常感谢!
\ $ \ endgroup \ $
– Sriv
20-2-19在11:56

\ $ \ begingroup \ $
我看不到您如何回答问题(为什么通常建议使用递归来计算斐波那契数列?这两种方法有哪些替代方法?),它没有对代码本身进行注释,而是使用图片显示文本,这是一种不好的形式,因为将来无法搜索和用作参考。前两点使您的答案看起来像是用于动态编程的广告,而不是代码复习,并且您应该使用引号和指向第三点来源的链接。
\ $ \ endgroup \ $
–加索
20-2-19在13:35

\ $ \ begingroup \ $
一个巨大的紫色矩形简直令人毛骨悚然,当向下滚动页面时,只会尖叫“广告!”。这导致我只是跳过得更远而将其从屏幕上移开。这里绝对没有理由要这样做。如果您要报价,请使用文字。尽管这一点似乎与请求的代码审查(IMHO)完全无关。
\ $ \ endgroup \ $
– 1201ProgramAlarm
20-2-19在17:18



\ $ \ begingroup \ $
@ 1201ProgramAlarm😂真好!我改变了
\ $ \ endgroup \ $
– Sriv
20-2-19在17:35

\ $ \ begingroup \ $
@gazoh你是对的。我认为OP一般要求递归,而不是专门针对斐波那契。我很抱歉我将尽快编辑我的答案!
\ $ \ endgroup \ $
– Sriv
20-2-19在17:38

#7 楼

评估渐进复杂度,例如使用gmp库显示,rcgldr的算法以O(log(n))互斥来实现有效的矩阵幂,在所提出的算法中性能最佳。


n步的直接迭代需要O(n ^ 1.60)的时间。 (n ^ 1.25)时间
时间为O(n ^ 1.029)的rcgldr算法。该图显示了n上以秒为单位的Fn的评估时间,两个轴均以10为底的对数,


#8 楼

尽管这个问题已经有很多答案,但其中一个是可以接受的,但我想指出的是,OP提出的(朴素的)递归解决方案比迭代版本的复杂性差得多。但是,完全有可能将问题分解为要由用户调用的主要功能,以及将其递归进行工作的内部帮助功能。下面的代码与OP的迭代解决方案具有相同的复杂度(实际上,它将由一个好的编译器编译为一个迭代解决方案),并且基本上由两个单行组成:

unsigned long long
fibonacci_internal(unsigned long long n,
                   unsigned long long t1,
                   unsigned long long t2) {
    return (n == 0) ? t1 : fibonacci_internal(n - 1, t2, t2 + t1);
}

unsigned long long fibonacci(unsigned long long n) {
    return fibonacci_internal(n, 0, 1);
}


编辑:修正了代码中的错字。递归调用发生在返回之前的逻辑分支的末尾,而在递归调用和返回之间没有其他操作。这称为尾递归。有关更多信息,请访问https://stackoverflow.com/questions/33923。 OP的原始函数在递归调用和返回之间添加了附加功能,因此它不是尾递归,递归调用必须使用额外的堆栈帧,并且编译器无法将其转换为迭代解决方案。

#9 楼

关于递归与迭代代码的争论是无止境的。有人说递归代码更“紧凑”并且更易于理解。在两种情况下(递归或迭代),当n即fib(n)的值增大时,系统上都会有一些“负载”。因此,fib( 5)将立即计算出来,但是fib(40)会在稍有延迟后显示。当然,您的数据类型也必须足够大才能保存结果.CI认为64位系统上的unsigned long long int是可以获取的最大数据,此外,您可能还想尝试将中间结果保存在数组。那么唯一的约束将是内存。

#10 楼

Binet给出的方程式:

fib [n] =(phi ^ n-(-phi)^(-n))/ sqrt(5)其中phi =(1 + sqrt(5)) / 2

将给出准确的答案(与上述fib [n] = phi ^ n / sqrt(5)+ 1/2的答案不同),当n的值大于70时,该值会分解)。

由于可以直接计算,因此不需要迭代和递归。

评论


\ $ \ begingroup \ $
1.请正确引用:“ [phi ^ n / sqrt(5)+ 1/2]”随附高斯括号! 2.如果您遵循上述数学公式,这是对Binet公式的准确简化,它对所有自然数(包括零)均有效。 3.对于较大的指数,C ++-双幂函数不准确。 Binet公式在具有IEEE double的C ++运行时中具有相同的依赖性和局限性。
\ $ \ endgroup \ $
–山姆·金里奇(Sam Ginrich)
20-2-22在23:21

\ $ \ begingroup \ $
感谢您纠正我对高斯括号的误解。我认为简化是正确的,因为采用了整数部分,因为Binet公式的第一部分(即phi ^ n / sqrt(5))在第二部分调整为的整数结果的正上方和正下方交替部分,即-(-phi)^(-n)/ sqrt(5)。换句话说,我的评论是不必要的,所以我谦虚地撤回了。 :)
\ $ \ endgroup \ $
–詹姆斯·邓拉普(James Dunlap)
20-2-25在4:10

#11 楼

最后,向

表示敬意,何时应该使用递归?

鉴于对算法的形式验证,您将编写一个不变式,它是该算法的数学模型,对然后您证明的任何变量和参数。当您的结果无论如何都定义为递归时(如斐波那契或阶乘级数所得出的那样),可以通过完全归纳来进行证明,其中归纳步骤很简单就是递归定义。例如,数量众多,则不会多次实例化函数的开销。

像C ++这样的运行时环境中,递归深度至关重要。您必须没有StackOverflow;如最初示例中那样,递归深度为O(n)是不可接受的!

因此,只要您可以控制渐近递归深度,并且运行时在评估中间结果时大部分是递归的

以下是对斐波那契数进行数位求值的算法,它使用了两个根据Binet公式和双曲函数的关系得出的整数序列;复杂度和递归深度为O(log(n))。



#include <iostream>

typedef unsigned long long N;

static void FibRec(int n, N& S, N&C)
{
    if (n >= 1)
    {
        N S1, C1;
        FibRec(n >> 1, S1, C1);
        if ((n >> 1) & 1)
        {
            C = (5 * C1 * C1 + S1 * S1) >> 1;
        }
        else
        {
            C = (C1 * C1 + 5 * S1 * S1) >> 1;
        }
        S= C1 * S1;
        if (n & 1)
        {
            N Cn0 = C;
            C = (C + S) >> 1;
            S= (5 * S+ Cn0) >> 1;
        }
    }
    else
    {
        S = 0;
        C = 2;
    }
}


N fibonacci(int n)
{
    N S, C;
    FibRec(n, S,C);
    return (n & 1) ? C : S;
}


int main()
{
    for (int n = 0; n<=93; n++)
    {
        std::cout << "Fib[" << n << "] = " << fibonacci(n) << std::endl;
    }
}