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


前言

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

递归求斐波那契数列

递归,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。
斐波那契数列的计算表达式很简单:

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

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

 1/*fibo.c*/ 2#include <stdio.h> 3#include <stdlib.h> 4/*求斐波那契数列递归版*/ 5unsigned long fibo(unsigned long int n) 6{ 7    if(n <= 1) 8        return n; 9    else 10        return fibo(n-1) + fibo(n-2);11}12int main(int argc,char *argv[])13{14    if(1 >= argc)15    {16       printf("usage:./fibo num\n");17       return -1;18    }19    unsigned long  n = atoi(argv[1]);20    unsigned long  fiboNum = fibo(n);21    printf("the %lu result is %lu\n",n,fiboNum);22    return 0;23}

关键代码为3~9行。简洁明了,一气呵成。
编译:

1gcc -o fibo fibo.c

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

1$ time ./fibo 52the 5 result is 534real    0m0.001s5user    0m0.001s6sys    0m0.000s

看起来并没有什么不妥,运行时间也很短。
继续计算第50个斐波那契数列:

1$ time ./fibo 502the 50 result is 1258626902534real    1m41.655s5user    1m41.524s6sys    0m0.076s

计算第50个斐波那契数的时候,竟然花了一分多钟!

递归分析

为什么计算第50个的时候竟然需要1分多钟。我们仔细分析我们的递归算法,就会发现问题,当我们计算fibo(5)的时候,是下面这样的:

 1                         |--F(1) 2                  |--F(2)| 3           |--F(3)|      |--F(0) 4           |      | 5    |--F(4)|      |--F(1) 6    |      |       7    |      |      |--F(1) 8    |      |--F(2)| 9    |             |--F(0)10F(5)|             11    |             |--F(1)12    |      |--F(2)|13    |      |      |--F(0)14    |--F(3)|15           |16           |--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中,我们可以通过下面的命令查看栈空间的软限制:

1$ ulimit -s28192

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

迭代解法

既然递归法不够优雅,我们换一种方法。如果不用计算机计算,让你去算第n个斐波那契数,你会怎么做呢?我想最简单直接的方法应该是:知道第一个和第二个后,计算第三个;知道第二个和第三个后,计算第四个,以此类推。最终可以得到我们需要的结果。这种思路,没有冗余的计算。基于这个思路,我们的C语言实现如下:

 1/*fibo1.c*/ 2#include <stdio.h> 3#include <stdlib.h> 4/*求斐波那契数列迭代版*/ 5unsigned long  fibo(unsigned long  n) 6{ 7    unsigned long  preVal = 1; 8    unsigned long  prePreVal = 0; 9    if(n <= 2)10        return n;11    unsigned long  loop = 1;12    unsigned long  returnVal = 0;13    while(loop < n)14    {15        returnVal = preVal +prePreVal;16        /*更新记录结果*/17        prePreVal = preVal;18        preVal = returnVal;19        loop++;20    }21    return returnVal;22}23/**main函数部分与fibo.c相同,这里省略*/

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

1$ gcc -o fibo1 fibo1.c2$ time ./fibo1 503the 50 result is 1258626902545real    0m0.002s6user    0m0.001s7sys    0m0.002s

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

尾递归解法

同样的思路,但是采用尾递归的方法来计算。要计算第n个斐波那契数,我们可以先计算第一个,第二个,如果未达到n,则继续递归计算,尾递归C语言实现如下:

 1/*fibo2.c*/ 2#include <stdio.h> 3#include <stdlib.h> 4/*求斐波那契数列尾递归版*/ 5unsigned long fiboProcess(unsigned long n,unsigned long  prePreVal,unsigned long  preVal,unsigned long begin) 6{ 7    /*如果已经计算到我们需要计算的,则返回*/ 8    if(n == begin) 9        return preVal+prePreVal;10    else11    {12        begin++;13        return fiboProcess(n,preVal,prePreVal+preVal,begin);14    }15}1617unsigned long  fibo(unsigned long  n)18{19    if(n <= 1)20        return n;21    else 22        return fiboProcess(n,0,1,2);23}2425/**main函数部分与fibo.c相同,这里省略*/

效率如何呢?

1$ gcc -o fibo2 fibo2.c2$ time ./fibo2 503the 50 result is 1258626902545real    0m0.002s6user    0m0.001s7sys    0m0.002s

可见,其效率并不逊于迭代法。尾递归在函数返回之前的最后一个操作仍然是递归调用。尾递归的好处是,进入下一个函数之前,已经获得了当前函数的结果,因此不需要保留当前函数的环境,内存占用自然也是比最开始提到的递归要小。时间复杂度为O(n)。

递归改进版

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

 1/*fibo3.c*/ 2#include <stdio.h> 3#include <stdlib.h> 4/*求斐波那契数列,避免重复计算版本*/ 5unsigned long fiboProcess(unsigned long *array,unsigned long n) 6{ 7    if(n < 2) 8        return n; 9    else10    {11        /*递归保存值*/12        array[n] = fiboProcess(array,n-1) + array[n-2];13        return array[n];14    }15}1617unsigned long  fibo(unsigned long  n)18{19    if(n <= 1)20        return n;21    unsigned long ret = 0;22    /*申请数组用于保存已经计算过的内容*/23    unsigned long *array = (unsigned long*)calloc(n+1,sizeof(unsigned long));24    if(NULL == array)25    {26        return -1;27    }28    array[1] = 1;29    ret = fiboProcess(array,n);30    free(array);31    array = NULL;32    return ret;33}34/**main函数部分与fibo.c相同,这里省略*/

效率如何呢?

1$ gcc -o fibo3 fibo3.c2$ time ./fibo3 503the 50 result is 1258626902545real    0m0.002s6user    0m0.002s7sys    0m0.001s

可见效率是不逊于其他两种优化算法的。但是特别注意的是,这种改进版的递归,虽然避免了重复计算,但是调用链仍然比较长。

其他解法

其他两种时间复杂度为O(logn)的矩阵解法以及O(1)的通项表达式解法本文不介绍。欢迎留言补充。

总结

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

  • 实现简单
  • 可读性好

缺点:

  • 递归调用,占用空间大
  • 递归太深,易发生栈溢出
  • 可能存在重复计算

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

微信公众号【编程珠玑】:专注但不限于分享计算机编程基础,Linux,C语言,C++,算法,数据库等编程相关[原创]技术文章,号内包含大量经典电子书和视频学习资源。欢迎一起交流学习,一起修炼计算机“内功”,知其然,更知其所以然。

原文地址:https://www.cnblogs.com/bianchengzhuji/p/10240897.html

时间: 2024-11-05 17:30:21

面试官问你斐波那契数列的时候不要高兴得太早的相关文章

一起talk C栗子吧(第四回:C语言实例--斐波那契数列)

各位看官们,大家好,从今天开始,我们讲大型章回体科技小说 :C栗子,也就是C语言实例.闲话休提, 言归正转.让我们一起talk C语言实例吧! 看官们,上一回中咱们说的是求阶乘的例子,这一回咱们说的例子是:斐波那契数列. 看官们,斐波那契数列是以数学家斐波那契数列的姓来命名的.斐波那契数列的定义:数列的第0项和第1项 为1,第n项为第n-1项和第n-2项的和.用数学公式表示出来就是:f(n)=f(n-1)+f(n-2),其中(n>1),f(n)=1;(n=0,1). 看官们,我在程序中使用了递归

EOJ 3506. 斐波那契数列

题意:给一个斐波那契数,问是斐波那契数列中的第几个,范围比较大是1到第1e5个斐波那契数 题解:选几个大质数MOD一下,预处理出范围内的所有膜后的值,如果输入的数在取模后能够和某一项斐波那契数的膜一一对应,那么他很大概率的就是它(类似hash???) p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px Menlo; color: #c81b13 } p.p2 { margin: 0.0px 0.0px 0.0px 0.0px; font: 1

还在用递归实现斐波那契数列,面试官一定会鄙视你到死

斐波那契数列指的是这样一个数列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368...... 我记得在初学C语言的时候,大学老师经常会讲一些常见的数学问题及递归的使用,其中斐波那契数列就是一定会被拿出来举例的.在后来工作中,面试做面试题的时候,也很大概率会出现编写算法实现斐波那契额数列求值.可以说,在我们编程道路上,编写算法实现斐波那契数列是

斐波那契数列 面试算法(三)

import java.math.BigDecimal; import java.util.Scanner; import java.util.function.BinaryOperator; public class Fbnq { /** * 假设n为正整数,斐波那契数列定义为: * f(n) = 1, n < 3; * f(n) = f(n-1) + f(n-2), n>=3 * * 现在请你来计算f(n)的值,但是不需要给出精确值,只要结果的后六位即可. * * 输入:一行,包含一个正整

剑指Offer面试题9(java版)斐波那契数列

题目一:写一个函数,输入n,求斐波那契数列的第n项.斐波那契数列的定义如下: 1.效率很低效的解法,挑剔的面试官不会喜欢 很多C语言的教科书在讲述递归函数的时候,都户拿Fibonacci作为例子,因此很多的应聘者对这道题的递归解法都很熟悉. 下面是实现代码 我们教科书上反复用这个问题来讲解递归的函数,并不能说明递归的解法最适合这道题目.面试官会提示我们上述递归的解法有很严重的效率问题要求我们分析原因. 我们以求解f(10)为例来分析递归的求解过程.想求得f(10),需要先求出f(9)和f(8).

斐波那契数列(C++ 和 Python 实现)

(说明:本博客中的题目.题目详细说明及参考代码均摘自 "何海涛<剑指Offer:名企面试官精讲典型编程题>2012年") 题目 1. 写一个函数,输入 n, 求斐波那契(Fibonacci)数列的第 n 项.斐波那契数列的定义如下: 2. 一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级.求该青蛙跳上一个n级的台阶总共有多少种跳法? 3. 一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级,...... ,也可以跳上n级,此时该青蛙跳上一个 n 级的台阶共有多少种跳法

斐波那契数列的实现算法

最近在看算法方面的书籍,看到了一个很古老的问题-斐波那契数列,这个题目在大学的时候肯定接触过,我们还在考试中考过,但是只是局限于当时课本上的内容,并没有仔细的考虑过这个题目的实现方法,今天就来小小的探究一下 最常见的实现算法就是递归,这个问题也是一个很基础的递归算法实现的例子: 求斐波那契数列第n个元素java代码来实现递归的方法如下: public static long FibonacciDemo1(long n) { if (n <= 0) return 0; if (n == 1) re

斐波那契数列问题的两种解决方法

斐波那契数列指的是这样一个数列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368........ 这个数列从第3项开始,每一项都等于前两项之和. 简单来说,斐波那契数列可以用下面这个公式来表示. { 0 ,n=0 f(n)={ 1 ,n=1 { f(n-1)+f(n-2) ,n>1 关于斐波那契数列衍生的算法题层出不穷,比如青蛙跳台阶问题等(

斐波拉契数列的计算方法

面试题9.斐波拉契数列 题目: 输入整数n,求斐波拉契数列第n个数. 思路: 一.递归式算法: 利用f(n) = f(n-1) + f(n-2)的特性来进行递归,代码如下: 代码: long long Fib(unsigned int n) { if(n<=0) return 0; if(n==1) return 1; return Fib(n-1) + Fib(n-2); } 缺陷: 当n比较大时递归非常慢,因为递归过程中存在很多重复计算. 二.改进思路: 应该采用非递归算法,保存之前的计算结