剑指offer-面试9:斐波那契数列(递归和循环)

如果需要重复地多次计算相同的问题,通常可以选择用递归或者循环两种不同的方法。递归是在一个函数的内部调用这个函数自身。而循环则是通过设置计算的初始值及终止条件,在一个范围内重复运算。
通常递归的代码会比较简洁。在上面的例子里,递归的代码只有一个语句,而循环则需要4个语句。在树的前序、中序、后序遍历算法的代码中,递归的实现明显要比循环简单得多。在面试的时候,如果面试官没有特别的要求,应聘者可以尽量多采用递归

递归虽然有简洁的优点,但它同时也有显著的缺点。递归由于是函数调用自身,而函数调用是有时间和空间的消耗的:每一次函数调用,都需要在内存栈中分配空间以保存参数、返回地址及临时变量,而且往栈里压入数据和弹出数据都需要时间。这就不难理解上述的例子中递归实现的效率不如循环。

另外,递归中有可能很多计算都是重复的,从而对性能带来很大的负面影响。递归的本质是把一个问题分解成两个或者多个小问题。如果多个小问题存在相互重叠的部分,那么就存在重复的计算。
除了效率之外,递归还有可能引起更严重的问题:调用栈溢出。前面分析中提到需要为每一次函数调用在内存栈中分配空间,而每个进程的栈的容量是有限的。当递归调用的层级太多时,就会超出栈的容量,从而导致调用栈溢出。

  • 题目
  • 分析
    • 效率很低的解法挑剔的面试官不会喜欢
    • 面试官期待的实用解法
    • 时间复杂度Ologn但不够实用的解法
    • 解法比较
  • 测试用例代码
  • 本题考点

题目

写一个函数,输入n,求斐波那契(Fibonacci)数列的第n项。斐波那契数列的定义如下:
这里写图片描述

分析

效率很低的解法,挑剔的面试官不会喜欢

很多的C语言教科书在将递归函数的时候,都会用Fibonacci作为例子,因此很多应聘者对这道题的递归解法都很熟悉

long long Fibonacci( unsigned int n )
{
    if( n<= 0 )
        return 0;
    if( n==1 )
        return 1;
    return Fibonacci( n-1 ) + Fibonacci( n-2 );
}

上述递归的解法有很严重的效率问题并要求我们分析原因。

以求解 f( 10 )为例来分析递归的求解过程。想求得 f(10),需要先求得 f(9) 和f(8)。同样,想求得 f(9),需要先求得 f(8) 和 f(7)。。。可以用树形结构来表示这种依赖关系。

可以发现这棵树中有很多结点时重复的,而且重复的结点数会随着n的增大而急剧增加,这意味计算量会随着n的增大而急剧增大。事实上,用递归方法计算的时间复杂度是以n的指数的方式递增的。

面试官期待的实用解法

其实改进的方法并不复杂,上述递归代码之所以慢是因为重复的计算太多,只要想办法避免重复计算就行了。比如可以把已经等到的数列中间项保存起来,如果下次需要计算的时候我们先查找一下,如果前面已经计算过就不用再重复计算了。

更简单的办法是从下往上计算,首先根据f(0)和f(1)计算出f(2),再根据f(1)和f(2)算出f(3)。。。依次类推就可以算出第n项了。很容易理解,这种思路的时间复杂度是O(n)。

long long Fibonacci( usigned n )
{
    int result[2] = { 0,1 };
    if( n<2 )
        return result[ n ];
    long long fibNMinusOne = 1;
    long long fibNMinusTwo = 0;
    long long fibN = 0;
    for( usigned int i=2; i<n; ++i )
    {
        fibN = fibNMinusOne + fibNMinusTwo;
        fibNMinusTwo = fibNMinusOne;
        fibNMinusOne = fibN;
    }
    return fibN;        
}

时间复杂度O(logn)但不够实用的解法

通常面试到这里也就差不多了,尽管我们还有比这更快的O(logn)算法。由于这种算法需要用到一个很生僻的数学公式,因此很少有面试官会要求我们掌握。以备不时之需。

解法比较

用不同的方法求解斐波那契数列的时间效率大不相同。
第一种基于递归的解法虽然直观但时间效率很低,在实际软件开发中不会用这种方法。
第二种方法把递归的算法用循环实现,极大地提高了时间效率。
第三种方法把求斐波那契数列转换成求矩阵的乘方,是一种很有创意的算法。虽然可以用O(logn)求得矩阵的n次方,但由于隐含的时间常数较大,很少有软件会采用这种算法。另外,这种解法的代码也很复杂,不太实用面试。

测试用例&代码

(1)功能测试(如输入3、5、10等)

(2)边界值测试(如输入 0、1、2)

(3)性能测试(输入较大的数字,如40、50、100等)

本题考点

(1)对递归、循环的理解及编码能力

(2)对时间复杂度的分析能力

(3)如果面试官采用的是青蛙跳台阶的问题,那同时还在考查应聘者的数学建模能力。

你可能感兴趣的:(递归,循环,斐波那契数列,剑指offer)