‍‍

作者 | 守望

责编 | 胡巍巍

前言

假如面试官让你编写求斐波那契数列的代码时,是不是心中暗喜?不就是递归么,早就会了。如果真这么想,那就危险了。

递归解法

递归,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。

斐波那契数列的计算表达式很简单:

F(n) = n; n = 0,1
F(n) = F(n-1) + F(n-2),n >= 2;

因此,我们能很快根据表达式写出递归版的代码:

/*fibo.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契数列递归版*/
unsigned long fibo(unsigned long int n)
{
    if(n <= 1)
        return n;
    else 
        return fibo(n-1) + fibo(n-2);
}
int main(int argc,char *argv[])
{
    if(1 >= argc)
    {
       printf("usage:./fibo num\n");
       return -1;
    }
    unsigned long  n = atoi(argv[1]);
    unsigned long  fiboNum = fibo(n);
    printf("the %lu result is %lu\n",n,fiboNum);
    return 0;
}

关键代码只有4行。简洁明了,一气呵成。

编译:

gcc -o fibo fibo.c

运行计算第5个斐波那契数:

$ time ./fibo 5
the 5 result is 5

real    0m0.001s
user    0m0.001s
sys    0m0.000s

看起来并没有什么不妥,运行时间也很短。

继续计算第50个斐波那契数列:

$ time ./fibo 50
the 50 result is 12586269025

real    1m41.655s
user    1m41.524s
sys    0m0.076s

计算第50个斐波那契数的时候,竟然将近两多钟!

递归分析

为什么计算第50个的时候竟然需要1分多钟。

我们仔细分析我们的递归算法,就会发现问题,当我们计算fibo(5)的时候,是下面这样的:

|--F(1)
                  |--F(2)|
           |--F(3)|      |--F(0)
           |      |
    |--F(4)|      |--F(1)
    |      |      
    |      |      |--F(1)
    |      |--F(2)|
    |             |--F(0)
F(5)|             
    |             |--F(1)
    |      |--F(2)|
    |      |      |--F(0)
    |--F(3)|
           |
           |--F(1)

为了计算fibo(5),需要计算fibo(3),fibo(4);而为了计算fibo(4),需要计算fibo(2),fibo(3)……

最终为了得到fibo(5)的结果,fibo(0)被计算了3次,fibo(1)被计算了5次,fibo(2)被计算了2次。可以看到,它的计算次数几乎是指数级的!

因此,虽然递归算法简洁,但是在这个问题中,它的时间复杂度却是难以接受的。

除此之外,递归函数调用的越来越深,它们在不断入栈却迟迟不出栈,空间需求越来越大,虽然访问速度高,但大小是有限的,最终可能导致栈溢出

在linux中,我们可以通过下面的命令查看栈空间的软限制:

$ ulimit -s
8192

可以看到,默认栈空间大小只有8M。

一般来说,8M的栈空间对于一般程序完全足够。如果8M的栈空间不够使用,那么就需要重新审视你的代码设计了。

递归改进版

既然我们知道最初版本的递归存在大量的重复计算,那么我们完全可以考虑将已经计算的值保存起来,从而避免重复计算,该版本代码实现如下:

/*fibo0.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契数列,避免重复计算版本*/
unsigned long fiboProcess(unsigned long *array,unsigned long n)
{
    if(n < 2)
        return n;
    else
    {
        /*递归保存值*/
        array[n] = fiboProcess(array,n-1) + array[n-2];
        return array[n];
    }
}

unsigned long  fibo(unsigned long  n)
{
    if(n <= 1)
        return n;
    unsigned long ret = 0;
    /*申请数组用于保存已经计算过的内容*/
    unsigned long *array = (unsigned long*)calloc(n+1,sizeof(unsigned long));
    if(NULL == array)
    {
        return -1;
    }
    array[1] = 1;
    ret = fiboProcess(array,n);
    free(array);
    array = NULL;
    return ret;
}
/**main函数部分与fibo.c相同,这里省略*/

效率如何呢?

$ gcc -o fibo0 fibo0.c
$ time ./fibo0 50
the 50 result is 12586269025

real    0m0.002s
user    0m0.000s
sys    0m0.002s

可见其效率还是不错的,时间复杂度为O(n)。

但是特别注意的是,这种改进版的递归,虽然避免了重复计算,但是调用链仍然比较长。

迭代解法

既然递归法不够优雅,我们换一种方法。如果不用计算机计算,让你去算第n个斐波那契数,你会怎么做呢?

我想最简单直接的方法应该是:知道第一个和第二个后,计算第三个;知道第二个和第三个后,计算第四个,以此类推。

最终可以得到我们需要的结果。这种思路,没有冗余的计算。基于这个思路,我们的C语言实现如下:

/*fibo1.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契数列迭代版*/
unsigned long  fibo(unsigned long  n)
{
    unsigned long  preVal = 1;
    unsigned long  prePreVal = 0;
    if(n <= 2)
        return n;
    unsigned long  loop = 1;
    unsigned long  returnVal = 0;
    while(loop < n)
    {
        returnVal = preVal +prePreVal;
        /*更新记录结果*/
        prePreVal = preVal;
        preVal = returnVal;
        loop++;
    }
    return returnVal;
}
/**main函数部分与fibo.c相同,这里省略*/

编译并计算第50个斐波那契数:

$ gcc -o fibo1 fibo1.c
$ time ./fibo1 50
the 50 result is 12586269025

real    0m0.002s
user    0m0.000s
sys    0m0.002s

可以看到,计算第50个斐波那契数只需要0.002s!时间复杂度为O(n)。

尾递归解法

同样的思路,但是采用尾递归的方法来计算。

要计算第n个斐波那契数,我们可以先计算第一个,第二个,如果未达到n,则继续递归计算,尾递归C语言实现如下:

/*fibo2.c*/
#include <stdio.h>
#include <stdlib.h>
/*求斐波那契数列尾递归版*/
unsigned long fiboProcess(unsigned long n,unsigned long  prePreVal,unsigned long  preVal,unsigned long begin)
{
    /*如果已经计算到我们需要计算的,则返回*/
    if(n == begin)
        return preVal+prePreVal;
    else
    {
        begin++;
        return fiboProcess(n,preVal,prePreVal+preVal,begin);
    }
}

unsigned long  fibo(unsigned long  n)
{
    if(n <= 1)
        return n;
    else 
        return fiboProcess(n,0,1,2);
}

/**main函数部分与fibo.c相同,这里省略*/

效率如何呢?

$ gcc -o fibo2 fibo2.c
$ time ./fibo2 50
the 50 result is 12586269025

real    0m0.002s
user    0m0.001s
sys    0m0.002s

可见,其效率并不逊于迭代法。尾递归在函数返回之前的最后一个操作仍然是递归调用。

尾递归的好处是,进入下一个函数之前,已经获得了当前函数的结果,因此不需要保留当前函数的环境,内存占用自然也是比最开始提到的递归要小。时间复杂度为O(n)。‍

矩阵快速幂解法

这是一种高效的解法,需要推导,对此不感兴趣的可直接看最终推导结果。

下面的式子成立是显而易见的,不多做解释。

如果a为矩阵,等式同样成立,后面我们会用到它。假设有矩阵2*2矩阵A,满足下面的等式:

可以得到矩阵A:

因此也就可以得到下面的矩阵等式:

再进行变换如下:

以此类推,得到:

实际上f(n)就是矩A^(n-1)中的A[0][0],或者是矩A^n中的A[0][1]。

那么现在的问题就归结为,如何求A^n,其中A为2*2的矩阵。

根据我们最开始的公式,很容易就有思路,代码实现如下:

/*fibo3.c*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_COL 2
#define MAX_ROW 2
typedef unsigned long MatrixType;
/*计算2*2矩阵乘法,这里没有写成通用形式,有兴趣的可以自己实现通用矩阵乘法*/
int matrixDot(MatrixType A[MAX_ROW][MAX_COL],MatrixType B[MAX_ROW][MAX_COL],MatrixType C[MAX_ROW][MAX_COL])
{
   /*C为返回结果,由于A可能和C相同,因此使用临时矩阵存储*/
    MatrixType tempMa[MAX_ROW][MAX_COL] ;
    memset(tempMa,0,sizeof(tempMa));
    /*这里简便处理*/
    tempMa[0][0] = A[0][0] * B[0][0] + A[0][1] * B [1][0];
    tempMa[0][1] = A[0][0] * B[0][1] + A[0][1] * B [1][1];
    tempMa[1][0] = A[1][0] * B[0][0] + A[1][1] * B [1][0];
    tempMa[1][1] = A[1][0] * B[0][1] + A[1][1] * B [1][1];
    memcpy(C,tempMa,sizeof(tempMa));

return 0;
}
MatrixType fibo(int n)
{
    if(n <= 1)
        return n;
    MatrixType result[][MAX_COL] = {1,0,0,1};
    MatrixType A[][2] = {1,1,1,0};
    while (n > 0) 
    {
        /*判断最后一位是否为1,即可知奇偶*/
        if (n&1) 
        {
            matrixDot(result,A,result);

}
        n /= 2;
        matrixDot(A,A,A);
    }
    return result[0][1];
}
/**main函数部分与fibo.c相同,这里省略*/

该算法的关键部分在于对A^n的计算,它利用了我们开始提到的等式,对奇数和偶数分别处理。

假设n为9,初始矩阵为INIT则计算过程如下:

  • 9为奇数,则计算INIT*A,随后A变为A*A,n变为9/2,即为4

  • 4为偶数,则结果仍为INIT*A,随后A变为,n变为4/2,即2

  • 2为偶数,则结果仍未INIT*A,随后变A变为 ,n变为2/2,即1

  • 1为奇数,则结果为INIT*(A^8)*A

可以看到,计算次数类似与二分查找次数,其时间复杂度为O(logn)。
运行试试看:

$ gcc -o fibo3 fibo3.c
$ time ./fibo3 50
the 50 result is 12586269025

real    0m0.002s
user    0m0.002s
sys    0m0.000s

通项公式解法

斐波那契数列的通项公式为:

关于通项公式的求解,可以当成一道高考数列大题,有兴趣的可以尝试一下(提示:两次构造等比数列)。C语言代码实现如下:

/*fibo4.c*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
unsigned long fibo(unsigned long n)
{
    if(n <=1 )
        return n;
    return (unsigned long)((pow((1+sqrt(5))/2,n)-pow((1-sqrt(5))/2,n))/sqrt(5));
}
/**main函数部分与fibo.c相同,这里省略*/

来看一下效率:

$ gcc -o fibo4 fibo4.c -lm
$ time ./fibo4
the 50 result is 12586269025

real    0m0.002s
user    0m0.002s
sys    0m0.000s

计算第50个,速度还不错。

列表法

如果需要求解的斐波那契数列的第n个在有限范围内,那么完全可以将已知的斐波那契数列存储起来,在需要的时候读取即可,时间复杂度可以为O(1)。

斐波那契数列应用

关于斐波那契数列在实际中很常见,数学上也有很多奇特的性质,有兴趣的可在百科中查看。

总结

总结一下递归的优缺点:
优点:

  • 实现简单

  • 可读性好

缺点:

  • 递归调用,占用空间大

  • 递归太深,易发生栈溢出

  • 可能存在重复计算

可以看到,对于求斐波那契数列的问题,使用一般的递归并不是一种很好的解法。
所以,当你使用递归方式实现一个功能之前,考虑一下使用递归带来的好处是否抵得上它的代价。

篇幅有限,不在此介绍,更多使用方法可以通过man命令名的方式去了解。

作者简介:守望,一名好文学,好技术的开发者。在个人公众号【编程珠玑】分享原创技术文章和学习资源,期待一起交流学习。

声明:本文为作者投稿,版权归作者个人所有。

45K!刚面完 AI 岗,这些技术必须掌握!

https://edu.csdn.net/topic/ai30?utm_source=csdn_bw

【End】

 热 文 推 荐 

☞GitHub 近 100,000 程序员“起义”:向“996”开炮!

☞小程序多端开源框架黑马!它是如何做到快应用小程序自由转译的?

☞正在被蚕食的百度搜索

☞云漫圈 | 写给对 ”游戏开发” 感兴趣的朋友们

☞NLP泰斗董振东老师与他的知网 | 纪念

☞10分钟狂赚800枚比特币? 这个边玩游戏边赚钱的涂鸦少年做到了!

☞交恶微软、拒绝乔布斯,21 岁的他是如何开发出 Linux 的?

☞现实!程序员只有跳槽才能涨薪吗?

System.out.println("点个在看吧!");
console.log("点个在看吧!");
print("点个在看吧!");
printf("点个在看吧!
");
cout << "点个在看吧!" << endl;
Console.WriteLine("点个在看吧!");
Response.Write("点个在看吧!");
alert("点个在看吧!")
echo "点个在看吧!"

点击阅读原文,输入关键词,即可搜索您想要的 CSDN 文章。

你点的每个“在看”,我都认真当成了喜欢

求职干货:再也不怕面试官问斐波那契数列了!相关推荐

  1. nodejs express use 传值_再也不怕面试官问你express和koa的区别了

    前言 用了那么多年的express.js,终于有时间来深入学习express,然后顺便再和koa2的实现方式对比一下. 老实说,还没看express.js源码之前,一直觉得express.js还是很不 ...

  2. 面试官问你斐波那契数列的时候不要高兴得太早 搞懂C语言函数指针 搜索引擎还可以这么玩? 那些相见恨晚的搜索技巧...

    面试官问你斐波那契数列的时候不要高兴得太早 前言 假如面试官让你编写求斐波那契数列的代码时,是不是心中暗喜?不就是递归么,早就会了.如果真这么想,那就危险了. 递归求斐波那契数列 递归,在数学与计算机 ...

  3. 看完这篇,我再也不怕面试官问垃圾收集了

    看完这篇,我再也不怕面试官问垃圾收集了 说在前面:本文的篇幅较长,看本文的时候最好先去上个厕所,先准备好一杯枸杞茶,慢慢品,本文将会讲解三种垃圾收集算法:标记-清除.复制.标记-整理算法,以及各种成熟 ...

  4. 面试官问你斐波那契数列的时候不要高兴得太早

    增加内容 递归改进版 矩阵快速幂解法 通项表达式解法 列表法 斐波那契数列应用 前言 假如面试官让你编写求斐波那契数列的代码时,是不是心中暗喜?不就是递归么,早就会了.如果真这么想,那就危险了. 递归 ...

  5. 用递归调用法求斐波那契函数_进阶版:面试官问你斐波那契数列的时候不要高兴得太早...

    增加内容 递归改进版 矩阵快速幂解法 通项表达式解法 列表法 斐波那契数列应用 前言 假如面试官让你编写求斐波那契数列的代码时,是不是心中暗喜?不就是递归么,早就会了.如果真这么想,那就危险了. 递归 ...

  6. c语言减治法求a的n次方算法,拜托,面试别再问我斐波那契数列了!!!

    面试中,问得比较多的几个问题之一,求斐波那契数列f(n)? 画外音:姐妹篇 <拜托,面试别再问我TopK了!!!> <拜托,面试别再让我数1了!!!> 什么是斐波那契数列? 斐 ...

  7. 拜托,面试别再问我斐波那契数列了!!!

    面试中,问得比较多的几个问题之一,求斐波那契数列f(n)? 画外音:姐妹篇 <拜托,面试别再问我TopK了!!!> <拜托,面试别再让我数1了!!!> 什么是斐波那契数列? 斐 ...

  8. Interview:算法岗位面试—上海某公司算法岗位(偏机器学习,互联网金融行业)技术面试考点之数据结构相关考察点—斐波那契数列、八皇后问题、两种LCS问题

    ML岗位面试:上海某公司算法岗位(偏机器学习,互联网金融行业)技术面试考点之数据结构相关考察点-斐波那契数列.八皇后问题.两种LCS问题 Interview:算法岗位面试-上海某公司算法岗位(偏机器学 ...

  9. 斐波拉契数列,有人买了一对小兔子,已知小兔子一个月后长成大兔子,大兔子每个月生一对小兔子,问:两年(24个月)之后,他一共有几对兔子。

    [01]斐波拉契数列,有人买了一对小兔子,已知小兔子一个月后长成大兔子,大兔子每个月生一对小兔子,问:两年(24个月)之后,他一共有几对兔子. 第i月份 大兔子 小兔子 总兔子 1 0 1 1 2 1 ...

最新文章

  1. python excel数据框_使用python pandas使用新数据框附加现有excel表
  2. mybatis批量更新
  3. 西门子伺服电机选型手册_记,新入行维修电工大胆拆解伺服电机和编码器的经历...
  4. rcnn spp_net hcp
  5. Linux系列 | 了解nohup和的功效
  6. 【ERNIE】深度剖析知识增强语义表示模型——ERNIE
  7. C/C++[codeup 1907]吃糖果
  8. C/C++[codeup 2043]小白鼠排队
  9. 批处理修改网关和dns服务器,[转载]使用批处理自动修改IP地址网关和DNS
  10. Spring 4.x 源码系列4-创建bean实例
  11. useCapture
  12. 局域网访问电脑中VMware虚拟机
  13. Bootstrap字体图标
  14. bilibili 网页版如何下载视频到本地(不用下载工具)
  15. 微型计算机普遍使用的编码是什么,微型计算机中普遍使用的字符编码是什么
  16. 程序员的自我修炼(一):打通任督二脉
  17. 年后离职潮,我该如何优雅的跟老板提离职?
  18. 关于使用pytorch在30系列显卡(高级别显卡)算力不够问题
  19. 多机器人集群网络通信协议分析
  20. 五个温度带的分界线_中温带与暖温带分界线

热门文章

  1. 【服务器】创建docker、运行jupyter相关命令
  2. 鸿蒙 自研内核 core b,华为平板将首次搭载鸿蒙OS 2.0系统:首次自研内核与构架...
  3. opencv 4计算机视觉项目实战_资源|计算机视觉实战操作(PDF下载)
  4. leetcode 题库46. 把数字翻译成字符串
  5. 【OpenCV】图片对比度和亮度
  6. 显示墙 显示服务器地址,云墙怎么看服务器地址
  7. jsp 访问mysql数据库_如何使用JSP访问MySQL数据库
  8. 面向对象编程启思录——读《OOD启思录》有感
  9. 库克谈iPhone 12供应紧张问题;2020中国互联网百强名单:阿里、腾讯、美团分列前三;Dgraph新版发布|极客头条
  10. 听说你的模型训练耗时太长?来昇腾开发者沙龙找解决方案