初窥门径 - 原来递归斐波那契如此慢

作者: holysu | 来源:发表于2017-05-28 00:31 被阅读157次

    初入算法的人都会听过斐波那契数列,看看维基百科的定义:

    斐波那契数列(意大利语 Successione di Fibonacci),又译为费波拿契数斐波那契数列费氏数列黄金分割数列
    在数学上,费波那契数列是以递归的方法来定义:

    用文字来说,就是费波那契数列由0和1开始,之后的费波那契系数就是由之前的两数相加而得出。首几个费波那契系数是:
    0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……

    如果以编程方式来获取第 N 个数的值,我很自然的就想到递归

    // in javascript
    function fibonacci(n){
        if(n <= 0) return 0;
        if(n == 1) return 1;
        return fibonacci(n-1) + fibonacci(n-2);
    }
    

    看着是不是很简洁? 嗯,很标准,边界确定了,也有一步步递进的求解过程,那我们放在浏览器 console 上跑一跑,顺便看看运行时间,为此再来帮助函数

    function time(fn, ...args) {
        var start = new Date().valueOf();
        console.log(fn(...args))
        var end = new Date().valueOf();
        console.log('elapsed: ' + (end - start) + 'ms');
    }
    

    先来获取 n = 10 的值,看下面的结果正常,1毫秒一开始也怎么在意。


    那再来试试 n = 50的... 等了好一会 还没打印出来结果 还以为浏览器卡了!!!


    这才到50, 就花去了330秒,5分多钟了喂,真可怕 !?(・_・;? 连三位数都还没到,更别说w级别以上了。

    为毛会这么慢??? 我们以 n = 10 来分析下过程


    先原谅我这丑不拉几的字迹... 沿着这个树形结构往回推进,发现Fib(8) 执行了2次,Fib(7)执行了3次,Fib(6)执行3次以上,存在着大量重复的计算,复杂度达到指数级别,而且数量级越大重复越严重。这二叉树没有满,我们按满的算那总的计算次数为 T(N) = 1 + 2 + 4 + 8 + ... + 2^10 => 2^n+1 -1 大概时间复杂度是 O( (2^N), 最牛逼的那个N次方。

    换个思路

    前面的递归是从第n位往回推导的,那我们正向的去一步步得出相应位置的值,就可以省去反向递归带来的大量重复操作了,也就是常说的递推,时间复杂度为 O(N) 代码如下:

    // 递推
    function fibonacci(n) {
        if (n <= 0) return 0;
        if (n == 1) return 1;
    
        var first = 0,
            second = 1,
            index = 1,
            fib = 0;
    
        while (index++ < n) {
            fib = first + second;
            first = second;
            second = fib;
        }
        return fib;
    }
    

    试验下, 50 几乎不耗时


    慢慢往上加, 可以看到到1w的时候 js的数字已经装不下了, 但是获取第10w位置的也才需要2ms的时间。


    补充@林晓池Smile 评论的说法, 以空间换时间,利用数组来存储已经计算过的斐波那契数列中的值

    var memo = [];
    function fibonacci(n){
        if(n <= 0) return 0;
        if(n == 1) return 1;
        if(!memo[n])
            memo[n] = fibonacci(n-1) + fibonacci(n-2);
        return memo[n];
    }
    

    测试看看



    可以看到相比一开始的递归,省去了很多重复的计算;但是获取第10w个数的时候堆栈就溢出了,因为即使记忆了但是还存在递归调用, 递推的方案倒是不会
    调用堆栈的上限如下:
    Three results:
    Node.js: 11034
    Firefox: 50994
    Chrome: 10402

    入门例子就可以体会到合理选择算法的威力了,我要继续努力~ 为分布式系统节省机器

    网上对斐波那契有其他更优质的算法,但是那种数学公司俺看不懂。。。 以后再回炉重造学习高数吧。

    相关文章

      网友评论

      • 林晓池Smile:记错了,刚重新跑一次,字典存储的跑999只需要0.2s 1000直接报错😂,普通的那种根本跑不完都😂😂
        holysu::no_mouth: 是时候好好学学数学了 :smirk:
      • 林晓池Smile:python中的斐波那契数列可以将之前算的结婚存到字典里这样不会每次重新计算,只要去字典里取值就行,曾经对比过用字典和不用字典的计算50的时候前者只需要2s后者要300多秒,而且计数越大,差距越明显,
        holysu:@林晓池Smile 哈哈 原来递归实现的 我一开始time(fibonacci,1000) 来着 然后半天没动 我就把浏览器给关了:smile: 然后点了下别的 发现卡住不动了, 后面弄成50放那边 自己喝奶茶看动漫 回头一看才知道跑的那么慢 哈哈哈
        林晓池Smile: @holysu 有个数忘了多少了,跑了半个多小时没跑完,怕把电脑玩坏直接关了😂
        holysu:@林晓池Smile 秒么 :fearful:那么可怕

      本文标题:初窥门径 - 原来递归斐波那契如此慢

      本文链接:https://www.haomeiwen.com/subject/zdhcfxtx.html