作者:何海涛

出处:http://zhedahht.blog.163.com/

题目:定义Fibonacci数列如下:

/  0                      n=0

f(n)=      1                      n=1

        \  f(n-1)+f(n-2)          n=2

输入n,用最快的方法求该数列的第n项。

分析:在很多C语言教科书中讲到递归函数的时候,都会用Fibonacci作为例子。因此很多程序员对这道题的递归解法非常熟悉,看到题目就能写出如下的递归求解的代码。

  1. ///////////////////////////////////////////////////////////////////////
  2. // Calculate the nth item of Fibonacci Series recursively
  3. ///////////////////////////////////////////////////////////////////////
  4. long long Fibonacci_Solution1(unsigned int n)
  5. {
  6. int result[2] = {0, 1};
  7. if(n < 2)
  8. return result[n];
  9.  
  10. return Fibonacci_Solution1(n - 1) + Fibonacci_Solution1(n - 2);
  11. }

但是,教科书上反复用这个题目来讲解递归函数,并不能说明递归解法最适合这道题目。我们以求解f(10)作为例子来分析递归求解的过程。要求得f(10),需要求得f(9)和f(8)。同样,要求得f(9),要先求得f(8)和f(7)……我们用树形结构来表示这种依赖关系

f(10)

               /        \

            f(9)         f(8)

          /     \       /    \

       f(8)     f(7)  f(7)   f(6)

      /   \     /   \ 

   f(7)  f(6)  f(6) f(5)

我们不难发现在这棵树中有很多结点会重复的,而且重复的结点数会随着n的增大而急剧增加。这意味这计算量会随着n的增大而急剧增大。事实上,用递归方法计算的时间复杂度是以n的指数的方式递增的。大家可以求Fibonacci的第100项试试,感受一下这样递归会慢到什么程度。在我的机器上,连续运行了一个多小时也没有出来结果。

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

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

  1. ///////////////////////////////////////////////////////////////////////
  2. // Calculate the nth item of Fibonacci Series iteratively
  3. ///////////////////////////////////////////////////////////////////////
  4. long long Fibonacci_Solution2(unsigned n)
  5. {
  6. int result[2] = {0, 1};
  7. if(n < 2)
  8. return result[n];
  9.  
  10. long long fibNMinusOne = 1;
  11. long long fibNMinusTwo = 0;
  12. long long fibN = 0;
  13. for(unsigned int i = 2; i <= n; ++ i)
  14. {
  15. fibN = fibNMinusOne + fibNMinusTwo;
  16.  
  17. fibNMinusTwo = fibNMinusOne;
  18. fibNMinusOne = fibN;
  19. }
  20.  
  21. return fibN;
  22. }

这还不是最快的方法。下面介绍一种时间复杂度是O(logn)的方法。在介绍这种方法之前,先介绍一个数学公式:

{f(n), f(n-1), f(n-1), f(n-2)} ={1, 1, 1,0}n-1

(注:{f(n+1), f(n), f(n), f(n-1)}表示一个矩阵。在矩阵中第一行第一列是f(n+1),第一行第二列是f(n),第二行第一列是f(n),第二行第二列是f(n-1)。)

有了这个公式,要求得f(n),我们只需要求得矩阵{1, 1, 1,0}的n-1次方,因为矩阵{1, 1, 1,0}的n-1次方的结果的第一行第一列就是f(n)。这个数学公式用数学归纳法不难证明。感兴趣的朋友不妨自己证明一下。

现在的问题转换为求矩阵{1, 1, 1, 0}的乘方。如果简单第从0开始循环,n次方将需要n次运算,并不比前面的方法要快。但我们可以考虑乘方的如下性质:

/  an/2*an/2                      n为偶数时

an=

        \  a(n-1)/2*a(n-1)/2            n为奇数时

要求得n次方,我们先求得n/2次方,再把n/2的结果平方一下。如果把求n次方的问题看成一个大问题,把求n/2看成一个较小的问题。这种把大问题分解成一个或多个小问题的思路我们称之为分治法。这样求n次方就只需要logn次运算了。

实现这种方式时,首先需要定义一个2×2的矩阵,并且定义好矩阵的乘法以及乘方运算。当这些运算定义好了之后,剩下的事情就变得非常简单。完整的实现代码如下所示。

  1. #include <cassert>
  2.  
  3. ///////////////////////////////////////////////////////////////////////
  4. // A 2 by 2 matrix
  5. ///////////////////////////////////////////////////////////////////////
  6. struct Matrix2By2
  7. {
  8. Matrix2By2
  9. (
  10. long long m00 = 0,
  11. long long m01 = 0,
  12. long long m10 = 0,
  13. long long m11 = 0
  14. )
  15. :m_00(m00), m_01(m01), m_10(m10), m_11(m11)
  16. {
  17. }
  18.  
  19. long long m_00;
  20. long long m_01;
  21. long long m_10;
  22. long long m_11;
  23. };
  24.  
  25. ///////////////////////////////////////////////////////////////////////
  26. // Multiply two matrices
  27. // Input: matrix1 - the first matrix
  28. // matrix2 - the second matrix
  29. //Output: the production of two matrices
  30. ///////////////////////////////////////////////////////////////////////
  31. Matrix2By2 MatrixMultiply
  32. (
  33. const Matrix2By2& matrix1,
  34. const Matrix2By2& matrix2
  35. )
  36. {
  37. return Matrix2By2(
  38. matrix1.m_00 * matrix2.m_00 + matrix1.m_01 * matrix2.m_10,
  39. matrix1.m_00 * matrix2.m_01 + matrix1.m_01 * matrix2.m_11,
  40. matrix1.m_10 * matrix2.m_00 + matrix1.m_11 * matrix2.m_10,
  41. matrix1.m_10 * matrix2.m_01 + matrix1.m_11 * matrix2.m_11);
  42. }
  43.  
  44. ///////////////////////////////////////////////////////////////////////
  45. // The nth power of matrix
  46. // 1 1
  47. // 1 0
  48. ///////////////////////////////////////////////////////////////////////
  49. Matrix2By2 MatrixPower(unsigned int n)
  50. {
  51. assert(n > 0);
  52.  
  53. Matrix2By2 matrix;
  54. if(n == 1)
  55. {
  56. matrix = Matrix2By2(1, 1, 1, 0);
  57. }
  58. else if(n % 2 == 0)
  59. {
  60. matrix = MatrixPower(n / 2);
  61. matrix = MatrixMultiply(matrix, matrix);
  62. }
  63. else if(n % 2 == 1)
  64. {
  65. matrix = MatrixPower((n - 1) / 2);
  66. matrix = MatrixMultiply(matrix, matrix);
  67. matrix = MatrixMultiply(matrix, Matrix2By2(1, 1, 1, 0));
  68. }
  69.  
  70. return matrix;
  71. }
  72.  
  73. ///////////////////////////////////////////////////////////////////////
  74. // Calculate the nth item of Fibonacci Series using devide and conquer
  75. ///////////////////////////////////////////////////////////////////////
  76. long long Fibonacci_Solution3(unsigned int n)
  77. {
  78. int result[2] = {0, 1};
  79. if(n < 2)
  80. return result[n];
  81.  
  82. Matrix2By2 PowerNMinus2 = MatrixPower(n - 1);
  83. return PowerNMinus2.m_00;
  84. }

作者:何海涛

出处:http://zhedahht.blog.163.com/

程序员面试题精选100题(16)-O(logn)求Fibonacci数列[算法]的更多相关文章

  1. 程序员面试题精选100题(33)-在O(1)时间删除链表结点[数据结构]

    作者:何海涛 出处:http://zhedahht.blog.163.com/ 题目:给定链表的头指针和一个结点指针,在O(1)时间删除该结点.链表结点的定义如下: struct ListNode { ...

  2. 程序员面试题精选100题(38)-输出1到最大的N位数[算法]

    作者:何海涛 出处:http://zhedahht.blog.163.com/ 题目:输入数字n,按顺序输出从1最大的n位10进制数.比如输入3,则输出1.2.3一直到最大的3位数即999. 分析:这 ...

  3. 16.O(logn)求Fibonacci数列[Fibonacci]

    [题目] log(n)时间Fib(n),本质log(n)求a^n. [代码]  C++ Code  12345678910111213141516171819202122232425262728293 ...

  4. 《面试题精选》15.O(logn)求Fibonacci数列

    题目:定义Fibonacci数列例如以下: /    0                      n=0 f(n)=      1                      n=1          ...

  5. Java程序员面试题集(1-50)(转)

    转:http://blog.csdn.net/jackfrued/article/details/17339393 下面的内容是对网上原有的Java面试题集及答案进行了全面修订之后给出的负责任的题目和 ...

  6. Java程序员面试题集(1-50

    下面的内容是对网上原有的Java面试题集及答案进行了全面修订之后给出的负责任的题目和答案,原来的题目中有很多重复题目和无价值的题目,还有不少的参考答案也是错误的,修改后的Java面试题集参照了JDK最 ...

  7. Java程序员面试题集(51-70)(转)

    转:http://blog.csdn.net/jackfrued/article/details/17403101 Java程序员面试题集(51-70) 摘要:这一部分主要讲解了异常.多线程.容器和I ...

  8. Java程序员面试题集(136-150)(转)

    转:http://blog.csdn.net/jackfrued/article/details/17740651 Java程序员面试题集(136-150) 摘要:这一部分主要是数据结构和算法相关的面 ...

  9. Java程序员面试题集(71-85)(转)

    转:http://blog.csdn.net/jackfrued/article/details/17566627 Java程序员面试题集(71-85) 摘要:这一部分主要包括了UML(统一建模语言) ...

随机推荐

  1. MySQL数据库加密与解密

    数据加密.解密在安全领域非常重要.对程序员而言,在数据库中以密文方式存储用户密码对入侵者剽窃用户隐私意义重大. 有多种前端加密算法可用于数据加密.解密,下面我向您推荐一种简单的数据库级别的数据加密.解 ...

  2. HDU 3452 Bonsai(网络流之最小割)

    题目地址:HDU 3452 最小割水题. 源点为根节点.再另设一汇点,汇点与叶子连边. 对叶子结点的推断是看度数是否为1. 代码例如以下: #include <iostream> #inc ...

  3. systemd service

    Man page systemd.unit SYSTEMD.UNIT(5) systemd.unit SYSTEMD.UNIT(5) NAME systemd.unit - Unit configur ...

  4. 连载:面向对象葵花宝典:思想、技巧与实践(32) - LSP原则

    LSP是唯一一个以人名命名的设计原则,并且作者还是一个"女博士"  ======================================================== ...

  5. hdu 5459 Jesus Is Here 数学

    Jesus Is Here Time Limit: 1 Sec Memory Limit: 256 MB 题目连接 http://acm.hdu.edu.cn/showproblem.php?pid= ...

  6. [Bootstap] 9. Dropdown

    Dropdown Arrow Class In order to create a down arrow like this: , what class should we apply to the ...

  7. 离线安装Cloudera Manager5.3.4与CDH5.3.4(二)

    Cloudera Manager Server和Agent所有后发先至.也能够进行CDH5的安装和配置. 然后,主节点可以通过浏览器访问7180port测试(因为CM Server需要花时间来启动,可 ...

  8. 【转】Oracle 10g RAC TAF

    本人转自:http://www.cnblogs.com/future2012lg/archive/2013/10/12/3365978.html Oracle RAC 同一时候具备HA(High Av ...

  9. android学习日记05--Activity间的跳转Intent实现

    Activity间的跳转 Android中的Activity就是Android应用与用户的接口,所以了解Activity间的跳转还是必要的.在 Android 中,不同的 Activity 实例可能运 ...

  10. 【C/C++多线程编程之六】pthread相互排斥量

    多线程编程之线程同步相互排斥量       Pthread是 POSIX threads 的简称,是POSIX的线程标准.          Pthread线程同步指多个线程协调地,有序地同步使用共享 ...