[转] 程序员面试题精选100题(16)-O(logn)求Fibonacci数列

news/2025/2/25 3:04:43
  题目:定义Fibonacci 数列如下:

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

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

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

///
// Calculate the nth item of Fibonacci Series recursively
///
long long Fibonacci_Solution1(unsigned int n)
{
      int result[2] = {0, 1};
      if(n < 2)
            return result[n];

      return Fibonacci_Solution1(n - 1) + Fibonacci_Solution1(n - 2);
}

但是,教科书上反复用这个题目来讲解递归函数,并不能说明递归解法最适合这道题目。我们以求解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(6)   f(5)
      /   \     /  \ 
   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)

///
// Calculate the nth item of Fibonacci Series iteratively
///
long long Fibonacci_Solution2(unsigned 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(unsigned int i = 2; i <= n; ++ i)
      {
            fibN = fibNMinusOne + fibNMinusTwo;

            fibNMinusTwo = fibNMinusOne;
            fibNMinusOne = fibN;
      }

 
      return fibN;
}

这还不是最快的方法。下面介绍一种时间复杂度是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的矩阵,并且定义好矩阵的乘法以及乘方运算。当这些运算定义好了之后,剩下的事情就变得非常简单。完整的实现代码如下所示。

#include <cassert>

///
// A 2 by 2 matrix
///
struct Matrix2By2
{
      Matrix2By2
      (
            long long m00 = 0, 
            long long m01 = 0, 
            long long m10 = 0, 
            long long m11 = 0
      )
      :m_00(m00), m_01(m01), m_10(m10), m_11(m11) 
      {
      }

      long long m_00;
      long long m_01;
      long long m_10;
      long long m_11;
};

///
// Multiply two matrices
// Input: matrix1 - the first matrix
//        matrix2 - the second matrix
//Output: the production of two matrices
///
Matrix2By2 MatrixMultiply
(
      const Matrix2By2& matrix1, 
      const Matrix2By2& matrix2
)
{
      return Matrix2By2(
            matrix1.m_00 * matrix2.m_00 + matrix1.m_01 * matrix2.m_10,
            matrix1.m_00 * matrix2.m_01 + matrix1.m_01 * matrix2.m_11,
            matrix1.m_10 * matrix2.m_00 + matrix1.m_11 * matrix2.m_10,
            matrix1.m_10 * matrix2.m_01 + matrix1.m_11 * matrix2.m_11);
}

///
// The nth power of matrix
// 1 1
// 1 0
///
Matrix2By2 MatrixPower(unsigned int n)
{
      assert(n > 0);

      Matrix2By2 matrix;
      if(n == 1)
      {
            matrix = Matrix2By2(1, 1, 1, 0);
      }
      else if(n % 2 == 0)
      {
            matrix = MatrixPower(n / 2);
            matrix = MatrixMultiply(matrix, matrix);
      }
      else if(n % 2 == 1)
      {
            matrix = MatrixPower((n - 1) / 2);
            matrix = MatrixMultiply(matrix, matrix);
            matrix = MatrixMultiply(matrix, Matrix2By2(1, 1, 1, 0));
      }

      return matrix;
}

///
// Calculate the nth item of Fibonacci Series using devide and conquer
///
long long Fibonacci_Solution3(unsigned int n)
{
      int result[2] = {0, 1};
      if(n < 2)
            return result[n];

      Matrix2By2 PowerNMinus2 = MatrixPower(n - 1);
      return PowerNMinus2.m_00;
}

博主何海涛对本博客文章享有版权。网络转载请注明出处http://zhedahht.blog.163.com/。整理出版物请和作者联系。

转载于:https://www.cnblogs.com/Jessy/archive/2011/02/09/1950248.html


http://www.niftyadmin.cn/n/674425.html

相关文章

ASP.NET 2.0中的异步页面

例子1&#xff1a;在当前页面获取另一个页面的内容&#xff1a;aspx&#xff1a;<% Page Async"true"...><asp:Label ID"lbOutPut" runat"server"></asp:Label>aspx.cs:Codeusing System.Net;using System.IO;using System.Te…

Linpack 安装和使用步骤

本文章参考了https://blog.csdn.net/kongfu_cat/article/details/78725907 一 简介 LINPACK是线性系统软件包(Linear system package) 的缩写。 Linpack现在在国际上已经成为最流行的用于测试高性能计算机系统浮点性能的benchmark。通过利用高性能计算机&#xff0c;用高斯…

不是老板胜似老板:香港9大打工皇帝

霍建宁&#xff0c;香港人。1952年生。毕业于香港大学,后赴美留学,取得专业会计师资格。1979年返港,加入长江实业集团,后升为会计主任。又考取澳大利亚特许会计师资格。1985年加入长实董事局为董事…… 在香港&#xff0c;霍建宁家喻户晓。许多媒体用日进斗金来形容他的收入。 …

ESX中的Linux热添加磁盘

给VM新加了一个磁盘&#xff0c;不想重启VM又想认出这个磁盘怎么办呢&#xff1f; 可以用这个命令&#xff1a; echo "scsi add-single-device A B C D" >/proc/scsi/scsi 参数 A、B、C、D 的内容是&#xff1a; A : SCSI HBA ID B : SCSI Channel C : SCSI ID D …

fillpolygon算法

填充算法很多&#xff0c;比如扫描线填充&#xff0c;种子填充等 http://www.codeproject.com/KB/GDI/QuickFill.aspx http://blog.sina.com.cn/s/blog_55a8a96d0100084k.html扫描线来源于光栅显示器的显示原理&#xff1a;对于屏幕上所有待显示像素的信息&#xff0c;将这些信…

HPL.dat 配置说明

原文链接&#xff1a;http://muchong.com/t-2238311-1-pid-3 并行平台构建与管理[8]&#xff1a;并行测试 GotoBLAShpl测试之L.dat各行意义及其设置 HPL.dat文件见附件 下面说说每行的意思和设置方法 1、2行&#xff0c;这个是说明性语句&#xff0c;不用更改。 3、4行&…

无符号数和有符号数(一) -- 原码表示法和补码表示法

无符号数&#xff1a; 即没有符号的数。 在c语言中就是 unsigned 类型的。 无符号数在计算机中的存储较为简单&#xff0c; 因为没有符号位&#xff0c; 直接将数字化成二进制然后存储在对应的存储器或者寄存器中。 这时寄存器或者存储器的位数就可以表示数值的范围&#xff0…

wmframework v2.0 手册(三)系统开发流程

WMframework提供了固定&#xff0c;规范的一体化代码编写过程&#xff0c;最终目的即为清晰的系统开发流程、减少开发人员代码编写时间并大大降低其工作量、方便项目组全局实现代码的风格的规范统一等 简要描述 1、确认页面元素 参照用户需求及相关功能设计文档&#xff0c;确认…