编程代码之战:第2章 斐波那契之旅

哔咔漫画
‹ 返回

第2章 斐波那契之旅

神圣罗马帝国皇室图书馆,海量的书架井然有序,一位头戴王冠的中年男子和一位学者在这里促膝长谈。

此时夜深人静,牛油烛散发出柔和的光芒,勉强能照亮一小块区域。

“斐波那契,你这个兔子问题,寡人想了很久都没有头绪”。

中年男子挠了挠头。

腓特烈二世生平最大的业余爱好便是研究数学。

而眼前这位学者,便是他的座上客,大名鼎鼎的斐波那契。

“兔生二月便能繁衍,视为成兔...”

“假定初月有一对幼兔,每月每对成兔可生一对幼兔,则一年后,可得兔几何?”

腓特烈自言自语着,沉浸其中。

(1,1,2,3...)

“二月之后可新生一对兔,故三月为两对兔,四月幼兔不足两月,无法繁殖,故为三对兔,以此类推...”

斐波那契耐心地解答道。

“哈哈哈哈!”

“爱卿果然才思敏捷!”

腓特烈二世竖起了大拇指,眼中满是赞赏之色。

“孤欲编纂算书,卿可为之...”

话说完,君臣两人离席。

斐波那契走在回住所的路上,脑海中却在回想刚才谈话的内容,似乎有所明悟。

“何不将该类问题,阐述为通项公式?”

他喃喃自语。

回到住所,斐波那契赶紧打开一个小册子,拿起鹅毛笔蘸了蘸墨水,写下刚才的想法。

“若有f(0)为0,f(1)为1,则f(n)为f(n-1)与f(n-2)之和”。

斐波那契抬头看了看窗外的月光,月下的树梢之上,停留着一只枯叶蝶。

那蝴蝶似乎有所感应,循着灯光,翩翩飞舞。

斐波那契还在专注地思考着,根本没有察觉这只蝴蝶正朝着他飞过来。

蝴蝶飞过窗台,然后轻轻地落在了斐波那契的肩头。

下一刻,它消失了,杨成的意识出现在了斐波那契脑海中。

“哇!”

杨成惊讶地看着自己这身古欧洲的学者服饰,摸了摸下巴。

他感觉自己的体貌特征来了个180度的大转变。

眼前的小册子在烛光下浮现出一行行字,顿时吸引了杨成的注意力。

“已知斐波那契通项公式f(n)=f(n-1)+f(n-2),编写求第N项斐波那契数的函数,N在20以内...”

杨成瞪大了眼睛,这里连电脑都没有,只有一支鹅毛笔,怎么写啊?

手写?

似乎问题也不是很大,求20项以内的斐波那契数,完全可以用简单的递归啊!

杨成回忆了一下,然后用鹅毛笔蘸了蘸墨水,在小册子上写下了寥寥几行。

这是一种“教科书式”的求解:

要求第N项,那么就分解为求第N-1项和第N-2项...

那N-1项又可以分解为求N-2和N-3项...

以此类推,直到N为0,返回0,N为1,返回1。

但这种方法之所以被称作教科书式,一是因为通俗易懂,二是因为效率很低下。

它求重复的项数太多了,或者说重复计算太多了,是一个指数级的算法。

杨成很清楚这种方法的弊端,但应付20以内的小数据量,绰绰有余哩!

果不其然,在杨成写完最后一个括号以后,手中的小册子绽放出一道金光。

不会爆出史诗装备来吧?

小册子犹如脱离了重力的束缚一般,慢慢浮空,它一页接一页地自动翻页,就好比有人在翻阅一般。

“啪嗒”。

小册子掉落在了桌子上,金光收敛,什么奇迹也没有发生...

杨成定睛一看,发现自己刚才手写的解题方法旁边多了一个小小的绿色对勾。

“唉,没啥挑战性啊”。

据说是萌新程序员必写的代码排行Top10...

杨成活动了一下筋骨。

这厮话音刚落。

然后,他看到那个小册子自动地翻过了一页,上面又浮现了一些笔迹。

“依上题,若N大于10000,且小于20000,作何解?”

杨成念完这新内容,皱了皱眉头。

“传统的递归方法求斐波那契数列,只限于小数求解,到了上万的数量级再用一般的递归,效率低不说,还有可能导致递归栈溢出”。

因为每次递归调用的时候,都会在栈里储存方法的参数、局部变量、返回值,像这样的一些信息。

而栈空间是有限的,像在早期的Windows系统中为1M大小。

所以,成千上万个此类信息空间累积起来,就容易爆掉它。

“那么,如何在原来的代码上面做修改,来达到提高性能的目的呢?”

杨成思索了片刻。

“既然递归方法慢的根本原因,在于重复性的计算太多,那么我可以使用缓存!”

杨成很快想到了解答方法,这得益于他有经常上博客论坛向大牛请教的习惯。

(Object{})

在JavaScript中,对象常用作为缓存,对于斐波那契数列这样的固定序列,用一个全局对象来缓存是比较合适的方法。

至于具体的逻辑,很好写:

假如缓存中没有这一项,那就缓存进去;如果存在,就直接把值取出来,无需重复计算。

(hash table)

JavaScript对象本质上是散列表,或者说哈希表。

所以这对象的存取效率高的很,都只需要常量时间,几乎可以忽略性能方面的开销了。

杨成在原本的解答上加了一些代码,用上了缓存的思想。

“这个题目加深了一些难度啊”。

杨成揉了揉太阳穴,看着那小册子再次犹如中了浮空术一般,晃悠悠地飞向了半空中,开始了不急不慢地翻页。

他看了看窗外,在那高高的塔楼顶端,还有卫兵在守卫。

这一切的一切都显得无比的真实。

他尝试着把手伸出窗外,却被一种无形的力量阻隔在了屋内。

一个系统音更是立刻响起:

“任务中,无法离开指定区域!”

他看了看四周,都是一些寻常人家的东西。

不过,当他看到了一个小小的架在木炭上面的咖啡壶,一个精致的骨瓷咖啡杯,还有一碗研磨得细细的咖啡粉...

杨成顿时有了个不错的想法。

我还需要一罐香浓的鲜牛奶...

一盒高品质的方糖...

一块最好的黑巧克力...

嗯,这样就能度过一段快乐的时光。

等杨成把这些都搞到手了,他嘴角还叼着一根冒着袅袅炊烟的软中华。

他一下子恢复了精神,而且无比的振奋。

嘿嘿,哥现在法力无边!

半空中,小册子的翻页速度越来越快,最后猛地一合拢,“啪嗒”一声过后,又掉落在了桌面上。

“这下子应该结束了吧”。

杨成翻开册子瞅了瞅。

在他刚才作答的那片区域旁边,又多了一个绿色对勾。

杨成感觉自己就像刚刚完成作业的一名小学生,在等着老师的批阅...

这小册子果然没有辜负他的期待,稍等了片刻,一行行笔迹就再次出现在了空白的地方。

“啪嗒”。

他手上的香烟黯然跌落...

杨成这次终于流露出一份凝重的表情,因为,这下子不是小修改,是大整改了!

“依上题所述,若N在100000到400000之间,作何解?”

杨成深吸一口气,在冷静地思考了五秒钟以后,他伸出一条长腿,把那根还未熄灭的香烟,一脚踩得干瘪。

烟雾散尽,香烟愉快地终结了它的使命,进入了废纸篓。

“我大概需要这个...”

他决定了,放弃递归,使用传统的线性方法,顺序遍历求解。

这里的递归不是顺序的,斐波那契数列的递归方法是一种深度遍历求解,递归栈中函数作用域对象的开辟和回收都需要很多额外的性能开销。

(Scope)

而顺序遍历则不存在这样的情况,它共享的是同一个作用域!

在早期的JavaScript中不支持块作用域,所以会共享同一个函数作用域或全局作用域。

因为公式f(n)= f(n-1)+ f(n-2)的缘故,要求第n项你只需要分别保存第n-1项和第n-2项的结果。

所以,可以使用两个临时的变量来保存,也就是只需要常量空间。

这样做,将算法的空间复杂度降到了最低,和递归庞大的保存栈相比,优势就太大了。

使用循环顺序遍历,可以显著地提高速度。

而不用担心,深度递归的函数因为“爆栈”的缘故导致运行失败...

那就真是一件让人遗憾的事情呢!

想清了思路,杨成正打算提笔就写,他却突然想到了一个令人震惊的后果。

对于JavaScript,数字类型是有大小限制的。

它在内部被表示为64位的浮点数,这和Java的double数字类型是一样的。

(Number.MAX_SAFE_INTEGER)

对于第几十万项的斐波那契数,它显然已经远远超出了范围!

那么,自己的这个算法会不会导致数值溢出,从而丢失掉精度?

好在他很快就想通了,关卡设计者怎么会考虑不到这样的问题,自己只要能写出正确的算法来就OK了。

在较新的JS标准中,提供了BigInt类型,可以做大整数的运算。

各种浏览器均已支持,语法很简洁,像这样:

BigInt(“10”)

或是更简单的定义:

10n

所以可以这样做1+1:

1n+1n

-----------------------

请知晓:它的各种操作,比方说加减乘除,不是常数时间复杂度的,只是给俺们提供便利的“语法糖”。

解释器会帮我们做转换~

-----------------------

这个方法其实并不难,杨成用十几行代码就搞定了。

小册子第三次浮空...

杨成舒服地打了个哈欠。

时间过得很慢...

这次小册子被翻页的时间和次数都多得多,显然和数据量大小有关系。

杨成甚至有些怀疑,是一台286电脑在充当服务器处理~

现代电脑有这么辣鸡嘛?

是不是并发量太大了,服务器被挤爆了的缘故呢?

(真相:这游戏确实是一个人开发维护的)

等到杨成开始怀疑这个小册子组件模块编写者,这厮性别取向问题的时候,小册子终于完成了它的使命…

“玛德,至少过去了半个钟头”。

杨成嘟哝着,再次翻开小册子某一页。

刚才写的那十几行代码旁边,又多了一个小小的对勾。

然而,还没来得及为自己高超的“手写代码”能力欢呼雀跃,杨成很快看到了让自己张大嘴巴的一个景象。

“嚯!”

他不禁倒吸了一口凉气。

随后,这厮犹如泄了气的皮球一般,倒在了后椅上。

“先前的思路又得改!”

“依上所述”。

这字迹依旧在忠实地记录着题目。

“若N在800000到1200000之间,作何解?”

这是一个典型的算法问题,要求高性能。

斐波那契传统的通项公式,已经无法满足这种需求了,或者说,已经被时代的前沿所抛弃了。

一般的通项公式面对这个问题,就如同蜗牛一样爬行,让人无法忍受。

需要用到的海量大整数加法运算,足以摧毁这种算法脆弱的体系。

这也恰恰体现了时代的局限性,毕竟斐波那契时代距今也相差近千年了。

杨成闭着眼睛,开始回忆以前在网上搜索的那一个个例子。

斐波那契矩阵,两倍项公式渐渐浮现在他的脑海中。

杨成嘴角咧出一丝笑意。

既然f(n)的公式不行,那就用f(2n)的公式!

他思索了片刻,用鹅毛笔蘸了蘸墨水,写下了一行公式:

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

这是一个对数级的算法,可以胜任大数据的挑战。

具体的代码他没有写,因为他并没有办法来验证程序的正确性,至于做单元测试,那更是想都别想。

令人惊讶的事情很快就发生了…

这个两倍项公式被一个椭圆的金色线条所环绕着,最后,旁边也出现了个对勾。

“叮!”

一声清脆的系统铃音响起。

“恭喜玩家您连续完成了阶段性任务,请休息一刻钟,我们将为您准备该系列的最后一项挑战!”

“唉”,杨成感觉有些乏味了。

这些题目确实比较益智,但总是一个人做,是不是太单调了?

于是,他灵机一动,打开玩家面板,选中了客服按钮。

“你好,很高兴为您服务!自助服务请按0,人工服务请按1”。

杨成选择了“1”。

“我是客服小美,请问您有什么问题嘛?”

那边传来了甜甜的妹子声音。

“唉呀,美女啊”。

这厮顿时来了兴致。

“我觉得你们的题目设计的很不错,但我有一个小小的建议啊”。

“请讲”。

客服妹子有耐心地问道。

“你看我一个人,穿着这样的奇装异服,在这里默默地做着题目,多枯燥啊”。

杨成摇摆着腿。

“嗯”,客服妹子表示理解。

“能不能安排一个类似于泰坦尼克号的双人解题环节,给俺试试啊”。

杨成坏坏地笑了。

“嗯...”

妹子有些无语了,这人真是想象力Max啊。

“好的,您的需求我们会尽可能考虑的”。

她体现了良好的职业素质。

“请问您还有什么需要帮助的嘛?”

“没了,我就想和漂亮姐姐你聊聊天啊”。

杨成脸上的笑意更浓烈了。

(这厮要准备耍流氓了)

“能不能和我真人视频哪?”

“祝您游戏愉快,再见,嘀,嘀...”

通讯设备那边见势不妙,很快就挂断了。

杨成有些不死心,再次选择了客服按钮。

“您拨打的客服热线正忙,请稍后再试...”

“您拨打的客服热线正忙,请稍后再试...”

“法克!”

杨成两手一摊,垂头丧气的。

So boring...

好在时间过得很快,一刻钟一下子就过去了。

杨成翻了翻小册子,很快发现了最后一道斐波那契系列的题目。

“这?”

杨成挠了挠头,这问题还真没有想过啊。

“让我想想,这该怎么算呢?”

他撕下一张纸,作为草稿,在上面演算起来。

还是做题目有意思...

“依上所述,若N为负数项,作何解?”

这字迹感觉是一个固定的格式,开头是“依上所述”,中间是“若N为X项”,后面则是“作何解?”。

杨成有点鄙视这个出题的人了,你就不能来一点新意嘛?

“求负数项有意义嘛?”

他不禁道出了心中的疑问。

比如说f(-1),这该怎么求呢?

杨成把f(-1)写在了f(0)和f(1)旁边,他仔仔细细地观察,很快发现了规律。

f(-1)不就是f(1)减去f(0)嘛!

f(-2)不就是f(0)减去f(-1)嘛!

那么,以此类推,将公式f(n)= f(n-1)+ f(n-2)简单变换一下,就能得到:

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

这不就是负数项公式了吗?

杨成把负数项公式填到小册子上,把它刚刚合上…

“噼啪噼啪!”

3D成像菜单顿时烟花齐放,系统制作的掌声如雷,系统声音也及时地响了起来。

“恭喜您成功完成了斐波那契之旅所有阶段的任务,您获得的积分明细如下”。

“初始积分2分”。

“递归方法完成斐波那契数列求解奖励2分”。

“缓存提高算法效率奖励2分”。

“线性求解奖励2分”。

“两倍项公式求解奖励5分”。

“负数项求解奖励2分”。

“现今共有积分15分,击败了全球10%的玩家,希望您再接再厉!”

杨成则是有些疲惫地抬了抬眼皮。

这些题目实在是太耗费脑力和体力了,自己都有些支撑不住了。

有必要吃个炒粉,喝点功能性维生素饮料,否则营养跟不上的话,怎么继续开车?

老司机又不是铁打的!

“请问您要继续挑战下一个关卡嘛?”

系统声音提示道。

“不必了,直接Esc吧”。

杨成摆了摆手。

“好的,祝您生活愉快,再见!”

眼前的世界骤然变黑,又瞬间恢复了视野,杨成摘掉VR头盔,揉了揉发酸的眼睛。

他这才发现网吧外面已经是一片漆黑,再不回去,估计寝室大门就关闭了。

对于翻墙这类问题,杨成还真不擅长,程序猿可不是猿猴…

在网吧楼下的小餐馆买了一份炒粉打包,再买了几瓶饮料,杨成这才走回了寝室。

室友们都在自己的笔记本前,玩一款流行了十多年的单机横版格斗游戏:

“毒奶粉”

杨成见状顿时耸耸肩,他大声嚷嚷道。

“这特么都二十年代了,你们还玩这08年出来的单机网游,太Out了吧?”

室友们愤愤不平地比了个中指,然后自顾自地玩去了。

杨成自讨了个没趣,便在书架里翻了又翻,摸索了半天后,他拿出一本不太薄也不太厚的《C专家编程》。

随后,这厮一个翻身爬上了铺位。

你说他挑这本经典书是为何?

莫不是想拿来装X?

非也,非也,你说这大日光灯下,不拿本书遮脸挡光,能睡得着嘛?

太厚了可压得生疼哩!

杨成把那《C专家编程》分开成两半,盖在脸上,闭上了眼睛。

他实在是太累了,很快便进入了睡梦中,与周公相会。

哔咔漫画