首頁 短篇

編程代碼之戰(zhàn)

第二章 斐波那契之旅

編程代碼之戰(zhàn) 程序小猿 5080 2020-02-27 11:55:07

  神圣羅馬帝國皇室圖書館,海量的書架井然有序,一位頭戴王冠的中年男子和一位學(xué)者在這里促膝長談。

  此時夜深人靜,牛油燭散發(fā)出柔和的光芒,勉強能照亮一小塊區(qū)域。

  “斐波那契,你這個兔子問題,寡人想了很久都沒有頭緒”。

  中年男子撓了撓頭。

  腓特烈二世生平最大的業(yè)余愛好便是研究數(shù)學(xué)。

  而眼前這位學(xué)者,便是他的座上客,大名鼎鼎的斐波那契。

  “兔生二月便能繁衍,視為成兔...”

  “假定初月有一對幼兔,每月每對成兔可生一對幼兔,則一年后,可得兔幾何?”

  腓特烈自言自語著,沉浸其中。

 ?。?,1,2,3...)

  “二月之后可新生一對兔,故三月為兩對兔,四月幼兔不足兩月,無法繁殖,故為三對兔,以此類推...”

  斐波那契耐心地解答道。

  “哈哈哈哈!”

  “愛卿果然才思敏捷!”

  腓特烈二世豎起了大拇指,眼中滿是贊賞之色。

  “孤欲編纂算書,卿可為之...”

  話說完,君臣兩人離席。

  斐波那契走在回住所的路上,腦海中卻在回想剛才談話的內(nèi)容,似乎有所明悟。

  “何不將該類問題,闡述為通項公式?”

  他喃喃自語。

  回到住所,斐波那契趕緊打開一個小冊子,拿起鵝毛筆蘸了蘸墨水,寫下剛才的想法。

  “若有f(0)為0,f(1)為1,則f(n)為f(n-1)與f(n-2)之和”。

  斐波那契抬頭看了看窗外的月光,月下的樹梢之上,停留著一只枯葉蝶。

  那蝴蝶似乎有所感應(yīng),循著燈光,翩翩飛舞。

  斐波那契還在專注地思考著,根本沒有察覺這只蝴蝶正朝著他飛過來。

  蝴蝶飛過窗臺,然后輕輕地落在了斐波那契的肩頭。

  下一刻,它消失了,楊成的意識出現(xiàn)在了斐波那契腦海中。

  “哇!”

  楊成驚訝地看著自己這身古歐洲的學(xué)者服飾,摸了摸下巴。

  他感覺自己的體貌特征來了個180度的大轉(zhuǎn)變。

  眼前的小冊子在燭光下浮現(xiàn)出一行行字,頓時吸引了楊成的注意力。

  “已知斐波那契通項公式f(n)=f(n-1)+f(n-2),編寫求第N項斐波那契數(shù)的函數(shù),N在20以內(nèi)...”

  楊成瞪大了眼睛,這里連電腦都沒有,只有一支鵝毛筆,怎么寫???

  手寫?

  似乎問題也不是很大,求20項以內(nèi)的斐波那契數(shù),完全可以用簡單的遞歸啊!

  楊成回憶了一下,然后用鵝毛筆蘸了蘸墨水,在小冊子上寫下了寥寥幾行。

  這是一種“教科書式”的求解:

  要求第N項,那么就分解為求第N-1項和第N-2項...

  那N-1項又可以分解為求N-2和N-3項...

  以此類推,直到N為0,返回0,N為1,返回1。

  但這種方法之所以被稱作教科書式,一是因為通俗易懂,二是因為效率很低下。

  它求重復(fù)的項數(shù)太多了,或者說重復(fù)計算太多了,是一個指數(shù)級的算法。

  楊成很清楚這種方法的弊端,但應(yīng)付20以內(nèi)的小數(shù)據(jù)量,綽綽有余哩!

  果不其然,在楊成寫完最后一個括號以后,手中的小冊子綻放出一道金光。

  不會爆出史詩裝備來吧?

  小冊子猶如脫離了重力的束縛一般,慢慢浮空,它一頁接一頁地自動翻頁,就好比有人在翻閱一般。

  “啪嗒”。

  小冊子掉落在了桌子上,金光收斂,什么奇跡也沒有發(fā)生...

  楊成定睛一看,發(fā)現(xiàn)自己剛才手寫的解題方法旁邊多了一個小小的綠色對勾。

  “唉,沒啥挑戰(zhàn)性啊”。

  據(jù)說是萌新程序員必寫的代碼排行Top10...

  楊成活動了一下筋骨。

  這廝話音剛落。

  然后,他看到那個小冊子自動地翻過了一頁,上面又浮現(xiàn)了一些筆跡。

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

  楊成念完這新內(nèi)容,皺了皺眉頭。

  “傳統(tǒng)的遞歸方法求斐波那契數(shù)列,只限于小數(shù)求解,到了上萬的數(shù)量級再用一般的遞歸,效率低不說,還有可能導(dǎo)致遞歸棧溢出”。

  因為每次遞歸調(diào)用的時候,都會在棧里儲存方法的參數(shù)、局部變量、返回值,像這樣的一些信息。

  而??臻g是有限的,像在早期的Windows系統(tǒng)中為1M大小。

  所以,成千上萬個此類信息空間累積起來,就容易爆掉它。

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

  楊成思索了片刻。

  “既然遞歸方法慢的根本原因,在于重復(fù)性的計算太多,那么我可以使用緩存!”

  楊成很快想到了解答方法,這得益于他有經(jīng)常上博客論壇向大牛請教的習(xí)慣。

 ?。∣bject{})

  在JavaScript中,對象常用作為緩存,對于斐波那契數(shù)列這樣的固定序列,用一個全局對象來緩存是比較合適的方法。

  至于具體的邏輯,很好寫:

  假如緩存中沒有這一項,那就緩存進去;如果存在,就直接把值取出來,無需重復(fù)計算。

  (hash table)

  JavaScript對象本質(zhì)上是散列表,或者說哈希表。

  所以這對象的存取效率高的很,都只需要常量時間,幾乎可以忽略性能方面的開銷了。

  楊成在原本的解答上加了一些代碼,用上了緩存的思想。

  “這個題目加深了一些難度啊”。

  楊成揉了揉太陽穴,看著那小冊子再次猶如中了浮空術(shù)一般,晃悠悠地飛向了半空中,開始了不急不慢地翻頁。

  他看了看窗外,在那高高的塔樓頂端,還有衛(wèi)兵在守衛(wèi)。

  這一切的一切都顯得無比的真實。

  他嘗試著把手伸出窗外,卻被一種無形的力量阻隔在了屋內(nèi)。

  一個系統(tǒng)音更是立刻響起:

  “任務(wù)中,無法離開指定區(qū)域!”

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

  不過,當他看到了一個小小的架在木炭上面的咖啡壺,一個精致的骨瓷咖啡杯,還有一碗研磨得細細的咖啡粉...

  楊成頓時有了個不錯的想法。

  我還需要一罐香濃的鮮牛奶...

  一盒高品質(zhì)的方糖...

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

  嗯,這樣就能度過一段快樂的時光。

  等楊成把這些都搞到手了,他嘴角還叼著一根冒著裊裊炊煙的軟中華。

  他一下子恢復(fù)了精神,而且無比的振奮。

  嘿嘿,哥現(xiàn)在法力無邊!

  半空中,小冊子的翻頁速度越來越快,最后猛地一合攏,“啪嗒”一聲過后,又掉落在了桌面上。

  “這下子應(yīng)該結(jié)束了吧”。

  楊成翻開冊子瞅了瞅。

  在他剛才作答的那片區(qū)域旁邊,又多了一個綠色對勾。

  楊成感覺自己就像剛剛完成作業(yè)的一名小學(xué)生,在等著老師的批閱...

  這小冊子果然沒有辜負他的期待,稍等了片刻,一行行筆跡就再次出現(xiàn)在了空白的地方。

  “啪嗒”。

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

  楊成這次終于流露出一份凝重的表情,因為,這下子不是小修改,是大整改了!

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

  楊成深吸一口氣,在冷靜地思考了五秒鐘以后,他伸出一條長腿,把那根還未熄滅的香煙,一腳踩得干癟。

  煙霧散盡,香煙愉快地終結(jié)了它的使命,進入了廢紙簍。

  “我大概需要這個...”

  他決定了,放棄遞歸,使用傳統(tǒng)的線性方法,順序遍歷求解。

  這里的遞歸不是順序的,斐波那契數(shù)列的遞歸方法是一種深度遍歷求解,遞歸棧中函數(shù)作用域?qū)ο蟮拈_辟和回收都需要很多額外的性能開銷。

 ?。⊿cope)

  而順序遍歷則不存在這樣的情況,它共享的是同一個作用域!

  在早期的JavaScript中不支持塊作用域,所以會共享同一個函數(shù)作用域或全局作用域。

  因為公式f(n)= f(n-1)+ f(n-2)的緣故,要求第n項你只需要分別保存第n-1項和第n-2項的結(jié)果。

  所以,可以使用兩個臨時的變量來保存,也就是只需要常量空間。

  這樣做,將算法的空間復(fù)雜度降到了最低,和遞歸龐大的保存棧相比,優(yōu)勢就太大了。

  使用循環(huán)順序遍歷,可以顯著地提高速度。

  而不用擔心,深度遞歸的函數(shù)因為“爆?!钡木壒蕦?dǎo)致運行失敗...

  那就真是一件讓人遺憾的事情呢!

  想清了思路,楊成正打算提筆就寫,他卻突然想到了一個令人震驚的后果。

  對于JavaScript,數(shù)字類型是有大小限制的。

  它在內(nèi)部被表示為64位的浮點數(shù),這和Java的double數(shù)字類型是一樣的。

 ?。∟umber.MAX_SAFE_INTEGER)

  對于第幾十萬項的斐波那契數(shù),它顯然已經(jīng)遠遠超出了范圍!

  那么,自己的這個算法會不會導(dǎo)致數(shù)值溢出,從而丟失掉精度?

  好在他很快就想通了,關(guān)卡設(shè)計者怎么會考慮不到這樣的問題,自己只要能寫出正確的算法來就OK了。

  在較新的JS標準中,提供了BigInt類型,可以做大整數(shù)的運算。

  各種瀏覽器均已支持,語法很簡潔,像這樣:

  BigInt(“10”)

  或是更簡單的定義:

  10n

  所以可以這樣做1+1:

  1n+1n

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

  請知曉:它的各種操作,比方說加減乘除,不是常數(shù)時間復(fù)雜度的,只是給俺們提供便利的“語法糖”。

  解釋器會幫我們做轉(zhuǎn)換~

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

  這個方法其實并不難,楊成用十幾行代碼就搞定了。

  小冊子第三次浮空...

  楊成舒服地打了個哈欠。

  時間過得很慢...

  這次小冊子被翻頁的時間和次數(shù)都多得多,顯然和數(shù)據(jù)量大小有關(guān)系。

  楊成甚至有些懷疑,是一臺286電腦在充當服務(wù)器處理~

  現(xiàn)代電腦有這么辣雞嘛?

  是不是并發(fā)量太大了,服務(wù)器被擠爆了的緣故呢?

 ?。ㄕ嫦啵哼@游戲確實是一個人開發(fā)維護的)

  等到楊成開始懷疑這個小冊子組件模塊編寫者,這廝性別取向問題的時候,小冊子終于完成了它的使命…

  “瑪?shù)?,至少過去了半個鐘頭”。

  楊成嘟噥著,再次翻開小冊子某一頁。

  剛才寫的那十幾行代碼旁邊,又多了一個小小的對勾。

  然而,還沒來得及為自己高超的“手寫代碼”能力歡呼雀躍,楊成很快看到了讓自己張大嘴巴的一個景象。

  “嚯!”

  他不禁倒吸了一口涼氣。

  隨后,這廝猶如泄了氣的皮球一般,倒在了后椅上。

  “先前的思路又得改!”

  “依上所述”。

  這字跡依舊在忠實地記錄著題目。

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

  這是一個典型的算法問題,要求高性能。

  斐波那契傳統(tǒng)的通項公式,已經(jīng)無法滿足這種需求了,或者說,已經(jīng)被時代的前沿所拋棄了。

  一般的通項公式面對這個問題,就如同蝸牛一樣爬行,讓人無法忍受。

  需要用到的海量大整數(shù)加法運算,足以摧毀這種算法脆弱的體系。

  這也恰恰體現(xiàn)了時代的局限性,畢竟斐波那契時代距今也相差近千年了。

  楊成閉著眼睛,開始回憶以前在網(wǎng)上搜索的那一個個例子。

  斐波那契矩陣,兩倍項公式漸漸浮現(xiàn)在他的腦海中。

  楊成嘴角咧出一絲笑意。

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

  他思索了片刻,用鵝毛筆蘸了蘸墨水,寫下了一行公式:

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

  這是一個對數(shù)級的算法,可以勝任大數(shù)據(jù)的挑戰(zhàn)。

  具體的代碼他沒有寫,因為他并沒有辦法來驗證程序的正確性,至于做單元測試,那更是想都別想。

  令人驚訝的事情很快就發(fā)生了…

  這個兩倍項公式被一個橢圓的金色線條所環(huán)繞著,最后,旁邊也出現(xiàn)了個對勾。

  “叮!”

  一聲清脆的系統(tǒng)鈴音響起。

  “恭喜玩家您連續(xù)完成了階段性任務(wù),請休息一刻鐘,我們將為您準備該系列的最后一項挑戰(zhàn)!”

  “唉”,楊成感覺有些乏味了。

  這些題目確實比較益智,但總是一個人做,是不是太單調(diào)了?

  于是,他靈機一動,打開玩家面板,選中了客服按鈕。

  “你好,很高興為您服務(wù)!自助服務(wù)請按0,人工服務(wù)請按1”。

  楊成選擇了“1”。

  “我是客服小美,請問您有什么問題嘛?”

  那邊傳來了甜甜的妹子聲音。

  “唉呀,美女啊”。

  這廝頓時來了興致。

  “我覺得你們的題目設(shè)計的很不錯,但我有一個小小的建議啊”。

  “請講”。

  客服妹子有耐心地問道。

  “你看我一個人,穿著這樣的奇裝異服,在這里默默地做著題目,多枯燥啊”。

  楊成搖擺著腿。

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

  “能不能安排一個類似于泰坦尼克號的雙人解題環(huán)節(jié),給俺試試啊”。

  楊成壞壞地笑了。

  “嗯...”

  妹子有些無語了,這人真是想象力Max啊。

  “好的,您的需求我們會盡可能考慮的”。

  她體現(xiàn)了良好的職業(yè)素質(zhì)。

  “請問您還有什么需要幫助的嘛?”

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

  楊成臉上的笑意更濃烈了。

 ?。ㄟ@廝要準備耍流氓了)

  “能不能和我真人視頻哪?”

  “祝您游戲愉快,再見,嘀,嘀...”

  通訊設(shè)備那邊見勢不妙,很快就掛斷了。

  楊成有些不死心,再次選擇了客服按鈕。

  “您撥打的客服熱線正忙,請稍后再試...”

  “您撥打的客服熱線正忙,請稍后再試...”

  “法克!”

  楊成兩手一攤,垂頭喪氣的。

  So boring...

  好在時間過得很快,一刻鐘一下子就過去了。

  楊成翻了翻小冊子,很快發(fā)現(xiàn)了最后一道斐波那契系列的題目。

  “這?”

  楊成撓了撓頭,這問題還真沒有想過啊。

  “讓我想想,這該怎么算呢?”

  他撕下一張紙,作為草稿,在上面演算起來。

  還是做題目有意思...

  “依上所述,若N為負數(shù)項,作何解?”

  這字跡感覺是一個固定的格式,開頭是“依上所述”,中間是“若N為X項”,后面則是“作何解?”。

  楊成有點鄙視這個出題的人了,你就不能來一點新意嘛?

  “求負數(shù)項有意義嘛?”

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

  比如說f(-1),這該怎么求呢?

  楊成把f(-1)寫在了f(0)和f(1)旁邊,他仔仔細細地觀察,很快發(fā)現(xiàn)了規(guī)律。

  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)

  這不就是負數(shù)項公式了嗎?

  楊成把負數(shù)項公式填到小冊子上,把它剛剛合上…

  “噼啪噼啪!”

  3D成像菜單頓時煙花齊放,系統(tǒng)制作的掌聲如雷,系統(tǒng)聲音也及時地響了起來。

  “恭喜您成功完成了斐波那契之旅所有階段的任務(wù),您獲得的積分明細如下”。

  “初始積分2分”。

  “遞歸方法完成斐波那契數(shù)列求解獎勵2分”。

  “緩存提高算法效率獎勵2分”。

  “線性求解獎勵2分”。

  “兩倍項公式求解獎勵5分”。

  “負數(shù)項求解獎勵2分”。

  “現(xiàn)今共有積分15分,擊敗了全球10%的玩家,希望您再接再厲!”

  楊成則是有些疲憊地抬了抬眼皮。

  這些題目實在是太耗費腦力和體力了,自己都有些支撐不住了。

  有必要吃個炒粉,喝點功能性維生素飲料,否則營養(yǎng)跟不上的話,怎么繼續(xù)開車?

  老司機又不是鐵打的!

  “請問您要繼續(xù)挑戰(zhàn)下一個關(guān)卡嘛?”

  系統(tǒng)聲音提示道。

  “不必了,直接Esc吧”。

  楊成擺了擺手。

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

  眼前的世界驟然變黑,又瞬間恢復(fù)了視野,楊成摘掉VR頭盔,揉了揉發(fā)酸的眼睛。

  他這才發(fā)現(xiàn)網(wǎng)吧外面已經(jīng)是一片漆黑,再不回去,估計寢室大門就關(guān)閉了。

  對于翻墻這類問題,楊成還真不擅長,程序猿可不是猿猴…

  在網(wǎng)吧樓下的小餐館買了一份炒粉打包,再買了幾瓶飲料,楊成這才走回了寢室。

  室友們都在自己的筆記本前,玩一款流行了十多年的單機橫版格斗游戲:

  “毒奶粉”

  楊成見狀頓時聳聳肩,他大聲嚷嚷道。

  “這特么都二十年代了,你們還玩這08年出來的單機網(wǎng)游,太Out了吧?”

  室友們憤憤不平地比了個中指,然后自顧自地玩去了。

  楊成自討了個沒趣,便在書架里翻了又翻,摸索了半天后,他拿出一本不太薄也不太厚的《C專家編程》。

  隨后,這廝一個翻身爬上了鋪位。

  你說他挑這本經(jīng)典書是為何?

  莫不是想拿來裝X?

  非也,非也,你說這大日光燈下,不拿本書遮臉擋光,能睡得著嘛?

  太厚了可壓得生疼哩!

  楊成把那《C專家編程》分開成兩半,蓋在臉上,閉上了眼睛。

  他實在是太累了,很快便進入了睡夢中,與周公相會。

按 “鍵盤左鍵←” 返回上一章  按 “鍵盤右鍵→” 進入下一章  按 “空格鍵” 向下滾動
目錄
目錄
設(shè)置
設(shè)置
書架
加入書架
書頁
返回書頁
指南