?!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
从斐波那契数列说?/p>
我想几乎每一个程序员Ҏ波那契(FibonacciQ数列都不会陌生Q在很多教科书或文章中涉及到递归或计复杂性的地方都会计斐波那契数列的E序作ؓl典CZ。如果现在让你以最快的速度用C#写出一个计斐波那契数列第n个数的函敎ͼ不考虑参数于1或结果溢出等异常情况Q,我不知你的程序是否会和下列代码类|
public static ulong Fib(ulong n)
{
return (n == 1 || n == 2) ? 1 : Fib(n - 1) + Fib(n - 2);
}
q段代码应该是短小_悍Q执行代码只有一行)Q直观清晎ͼ而且非常W合许多E序员的代码学Q许多h在面试时写出q样的代码可能心里还会暗爽。但是如果用q段代码试试计算Fib(100)我想再也爽不v来了Q估计下星期甚至下个月前l果很难得出来?/p>
看来好看的代码未必中用,如果E序在效率不能接受那观马的就都是云了。如果简单分析一下程序的执行,׃发现问题在哪Q以计算Fibonacci(5)ZQ?/p>
从上囑֏以看出,在计Fib(5)的过E中QFib(1)计算了两ơ、Fib(2)计算?ơ,Fib(3)计算了两ơ,本来只需?ơ计就可以完成的Q务却计算?ơ。这个问题随着规模的增加会愈发凸显Q以至于Fib(100)已经无法再可接受的时间内出。虽然可以通过N归优化双递归变ؓ单递归Q但是效果也q不理想?/p>
q是一个非常典型的忽视“计复用”的例子。计复用的目标在于保证计算q程中同一计算子过E只q行一ơ,通过保存子过E计结果ƈ复用来提高计效率。其实类g面的代码出现在很多教U书中,如果是ؓ了展C斐波那契数列的数学Ҏ当然无可厚非,但是作ؓ计算机程序就很有问题了。因为数学和计算U学是有区别的,数学要求严}和简z的表达Q而计科学则需要尽量快的得出结果,好的数学公式未必是好的计公式。这也说明程序设计不是简单的数学语a译机语言可以了Q程序员应该能将数学语言首先译成计科学语aQ算法?Q,然后再翻译成机器语言。因此程序员的工作绝不是机械的,而是要有一定的创造性,所以必要的法知识对程序员臛_重要Q因为算法教会程序员如何用最有效率的方式ȝ写程序?/p>
a归正传,Ҏ以上分析Q可以写Z个更高效的斐波那契数列计程序:
public static ulong Fib(ulong n)
{
if (n == 1 || n == 2)
{
return 1;
}
ulong m1 = 1, m2 = 1;
for (ulong i = 3; i <= n; i++)
{
m2 = m1 + m2;
m1 = m2 - m1;
}
return m2;
}
q段代码可能看v来不如上一D那么优,但是其效率却是第一D代码不可比拟的。例如计Fib(40)Q在我的机器上,W一D代码用?.5U,而第二段代码于0.001U。这个差距随着规模增大会更明显Q例如Fib(100)Q第一D代码可能需要几天甚臛_周,而第二段代码耗时仍然于0.001U。天壤之别!
如果从计复杂性的角度分析Q第一D代码的复杂度ؓO(1.6^n)Q对数学敏感的朋友应该能体会到这个函数可怕的增长速度Q这甚至不是一个多式U别的复杂度Q而第二段代码仅ؓO(n)。看到如此简单一个例子出现如此差别,q能说程序员学习法没有用吗?/p>
上面代码对于“计复用”的思想体现不是很明显,因ؓ我们仅仅需要一个结果,中间l果都被丢弃了,如果是计?<=i<=n的所有Fib(i)Q那么计复用的思想׃体现的比较明显?/p>
矩阵乘法与Strassen法
下面说一个将计算复用发挥到极致的例子Q说实话直到现在每次看到Strassen法我都觉得震撼Q不知Strassen当年是长了何{天才的脑子才发现这么漂亮的一个算法?/p>
矩阵计算在许多领域如机器学习、图形图像处理、模式识别中均占有重要地位。而计两个n*n矩阵乘积的运是矩阵计算中常见的计算。由矩阵理论可知Q普通方法计两个n阶方늚乘积需要进行n^3ơ乘法计,其计复杂度自然是O(n^3)。但是d国数学家Volker Strassen通过拆分矩阵q复用计结果,发现了一U复杂度为O(n^2.81)的算法,q个法单说来如下?/p>
假设n?的幂Q不?的幂也能计算Q这里是Z方便说明Q,A和B是两个n阶方阵,则A和B分别可以分解?个n/2阶方阵:
则:
可惜q样l过8ơn/2阶方늛乘,复杂度还是O(n^3)Q没有降低复杂度。天才的Volker Strassen发现了一U通过计算7ơn/2阶方阉|得出n阶方阵乘U的Ҏ。具体来_设:
注意在计P1-P7q程中,虽然双共有20个子矩阵乘积Q但是去重复后只有aeQbgQafQbhQceQdgQcfQdhQ只?个!因此通过计算复用的思想可以通过7ơ子矩阵乘积l果计算出P1-P7Q然后:
q样q合出了ABQ这个方法的复杂度ؓO(n^2.81)Q这个算法实在是太漂亮了。天才!l对的天才啊Q对于这Uh除了无限崇敬我真是没有其它想法了Q能计复用发挥到如此境地Q不知世间能有几人?/p>
计算复用对Y件开发的启示
也许有的朋友会说Q“我又不开发数D型E序Q也不会接触如此复杂的算法,计算复用与我何干Q”。实际上即开发非数值型E序Q计复用的思想也是大有用途的。例如我曄在一个真实的PHP开发的行业pȝ中见q类DL代码Q?/p>
foreach($items as $k => $v){
//...
$money = $v->money + getTax();
//...
}
当时我问开发这个程序的里getTax的返回值和每个item有关pdQ他说税Ҏ一套复杂的法出来的Q但是其值固定的。那q里可就太浪费了Q每ơ@环都计算一ơ,如果改ؓ如下Q?/p>
$tax = getTax();
foreach($items as $k => $v){
//...
$money = $v->money + $tax;
//...
}
则可以节省不计资源。在后来的沟通中发现q个问题原来是重构的遗留问题Q以前系l中的税率计是写在E序里的Q后来发现这个计越来越多,׃用“Extract Method”重构模式提取成了getTax函数Q但是这L后果是到处都是getTax调用Q有的程序段甚至调用七八ơ,但是如果应用计算复用的思想Q则应该在脚本开始只计算一ơ税费ƈ保存Q后面全都用这个变量而不是每ơ调用getTax?/p>
MQ只要某个计结果与执行上下文无养Iq且在一个执行流中超q一ơ被使用Q则应该使用计算复用?/p>
q个例子q算明显的,有时可能不会q么明显Q例如我们知道JavaScript中从深层函数中引用全局对象的代h很高的,因ؓ需要遍历作用域链(当然是隐式的Q,因此在JS中如果深层函C码频J用全局对象Q则要付出很高的代h。如果程序员不懂得对象及作用域链相关知识Q则不会发现q种潜在的效率问题,而正的做法是用一个局部变量保存对全局对象的引用而不是每ơ都直接使用全局变量?/p>
很多成熟的品也处处体现着计算复用的思想Q如在PHP中,下面代码可以得到一个数l的元素个数Q?/p>
echo count($arr);
如果我们来实玎ͼ最自然的想法就是遍历数l。但是PHP的开发者明显更聪明Q他们在建立数组时同时徏立一个与之关联的内部的数量计数变量(对PHPE序员透明Q,随着数组元素的增减,q个变量也相应增减,每次调用count函数直接q回q个变量卛_Q这将count的复杂度从O(n)降ؓO(1)Q这也是计算复用的一个典型应用?/p>
另外Q其实计复用和~存的概忉|盔R的Q很多缓存系l就使用了计复用的思想?/p>