文章目录
0. 前言
介绍下求解Fibonacci数算法的优缺点。
数列递归形式:
1. 二分递归版 O( 2 n 2^n 2n)
1.1 代码与问题
__int64 fib ( int n ) { //计算Fibonacci数列的第n项(二分递归版):O(2^n)
return ( 2 > n ) ?
( __int64 ) n //若到达递归基,直接取值
: fib ( n - 1 ) + fib ( n - 2 ); //否则,递归计算前两项,其和即为正解
}
该算法需要运行O(
2
n
2^n
2n)时间才能计算出第n个Fibonacci数。这一指数复杂度的算法,在实际环境中毫无价值。
CPU资源未被占满,进程在满负荷运行,但是依然运行地非常非常慢。
1.2 复杂度分析
上述推导看复杂度为指数(
2
n
2^n
2n)
估算下
ϕ
43
\phi^{43}
ϕ43 =
2
30
2^{30}
230 =
1
0
9
10^{9}
109flo = 1 sec(计算工作量大概可以用10地9次方条基本操作指令来度量,10的9次方就是现在主流计算机主频的cpu在1s中大致能够吞吐的计算量,亦是fib(43)计算大概需要1s)
1.3 递归分析
使用第二种手段对上述代码进行分析
究其原因在于,计算过程中所出现的递归实例的重复度极高!!!
1.4 优化策略
借助一定量的辅助空间,在各子问题求解之后,及时记录下其对应的解答,使得每个递归实例从原理上来讲只需要计算一次。
2. 线性递归 O(n)
2.1 代码示例
将Fibonacci数的各项所计算的结果制作成一个表格,将每项计算结果存入表中,每次需要递归实例的时候便可以查表获取。智能屏蔽此次和后续调用。在O(1)时间内返回结果。
__int64 fib ( int n, __int64& prev ) { //计算Fibonacci数列第n项(线性递归版):入口形式fib(n, prev)
if ( 0 == n ) //若到达递归基,则
{ prev = 1; return 0; } //直接取值:fib(-1) = 1, fib(0) = 0
else { //否则
__int64 prevPrev; prev = fib ( n - 1, prevPrev ); //递归计算前两项
return prevPrev + prev; //其和即为正解
}
} //用辅助变量记录前一项,返回数列的当前项,O(n)
2.2 时间复杂度和空间复杂度
该算法呈线性递归模式,递归的深度线性正比于输入n,前后共计仅出现O(n)个递归实例,累计耗时不超过O(n)。遗憾的是,该算法共需使用O(n)规模的附加空间。
3. 迭代 O(n)
3.1 代码示例
计算方向为自小而大,自底而上,这样由原来的递归算法改进为迭代型算法,同样可以完成刚才工作。如果比作上楼梯,那么在每个台阶上只需停留一次。
例如fib(5):
f(5) = f(4) + f(3)
f(4) = f(3) + f(2)
f(3) = f(2) + f(1)
f(2) = f(1) + f(0)
f(1) = f(0) + f(-1)
迭代的计算是自底而上,所以是算f(1) f(2) f(3)f(4) f(5)
g = g+f 等同于 f(1) = f(0) + f(-1) 再迭代
g = g+f 等同于 f(2) = f(1) + f(0) ,
这次f(0)是未知的,所以f = g-f就是算出下次迭代的末项f(0)
f(0) = f(1) - f(-1) 等同于 f = g-f
__int64 fibI( int n ) { //计算Fibonacci数列的第n项(迭代版):O(n)
__int64 f = 1, g = 0; //初始化:fib(-1)、fib(0)
while ( 0 < n-- ) {
g += f;
f = g - f;
} //依据原始定义,通过n次加法和减法计算fib(n)
return g; //返回
}
3.2 时间复杂度和空间复杂度
这里仅使用了两个中间变量f和g,记录当前的一对相邻Fibonacci数。整个算法仅需线性步
的迭代,时间复杂度为O(n)。更重要的是,该版本仅需常数规模的附加空间,空间效率也有了极大提高。可以认为空间复杂度是个常数O(1)。
4. 封装对象
迭代版 fibI()算法,实现支持如下接口的 Fib 类。
class Fib { //Fibonacci数列类
private:
Rank f, g; //f = fib(k - 1), g = fib(k)。均为int型,很快就会数值溢出
public:
Fib ( Rank n ) //初始化为不小于n的最小Fibonacci项
{ f = 1; g = 0; while ( g < n ) next(); } //fib(-1), fib(0),O(log_phi(n))时间
Rank get() { return g; } //获取当前Fibonacci项,O(1)时间
Rank next() { g += f; f = g - f; return g; } //转至下一Fibonacci项,O(1)时间
Rank prev() { f = g - f; g -= f; return g; } //转至上一Fibonacci项,O(1)时间
};
为定位至不小于n的最小项,以下反复调用next()接口依次递增地遍历,直至首次达到或者超过n。因n与Fibonacci数的第 l o g ϕ log_\phi logϕ(n)项渐进地同阶,故整个遍历过程不超过 l o g ϕ log_\phi logϕ(n)步。
next()接口对f和g的更新方式,与fibI()算法完全一致。
取前一项的方法亦与此相似,只不过方向颠倒而已。