遞歸的邏輯(1)——遞歸關系模型
查爾斯·巴貝奇是一名19世紀的英國發明家,也被說成是職業數學家。他曾經發明了差分機——一臺能夠按照設計者的意圖,自動處理不同函數的計算過程的機器。這是一臺碩大的、泛著微光的金屬機器,包括數以千計加工精密的曲柄和齒輪。他在孤軍奮戰下造出的這臺機器,運算精度達到了6位小數,能夠算出好幾種函數表。此後的實際運用證明,這種機器非常適合於編制航海和天文方面的數學用表。
巴貝奇花費了漫長的一生來改進差分機,先是一種設想,然後是另一種設想。他曾設想用儲存數據穿孔卡上的指令進行任何數學運算的可能性,並設想了現代計算機所具有的大多數其他特性。然而這些都超越了他所處的時代——巴貝奇的設想直到電子時代才得以完成——改進版的差分機最終沒有變成現實。
在一次科技展覽會上,年輕而又光彩照人的奧古斯塔·愛達·拜倫看到了被稱為“思考機器”的差分機試驗品。那一刻,愛達確定了她一生的目標——完成這臺精美的機器。接下來的幾年裏,愛達迅速學會了各種數學知識並拜入巴貝奇門下,終其一生讓處理數的差分機變成處理信息的分析機。
為了展示機器的威力,愛達曾設計了一個假想的程序,它循環運行,一次叠代的結果將成為下一次叠代的輸入,每個函數前後相繼,遵循相同的規則,巴貝奇將這個思路稱為“機器咬尾巴——團團轉”。然而這個程序僅僅存在於愛達的頭腦中,直到她去世也沒有生產出可以運行這個程序的機器。
一個世紀後,愛達的夢想終於變成了現實,她的假想也成為程序員們的一種重要算法——遞歸。
遞歸關系式
先來看一個表達式:
表達式指出從第3項開始,每一項都是由前兩項通過計算得出的,這類表達式就是遞歸關系式,有時也稱為差分方程。為了能從遞歸關系式計算出序列中的每一項,必須知道序列開始的若幹個數,這些數被稱為初始條件或初始值。
由於采取逐步計算的方式可以得到序列各項的值,所以很多時候得到遞歸關系是本身就是朝解決一個計數問題邁了一大步。有些時候甚至只能依賴遞歸關系進行計算。
不斷繁殖的兔子——遞歸關系模型
最著名的遞歸模型當屬斐波那契(Fibonacci)數列,它最早出現在1202年。
斐波那契提出了一個關於兔子的問題:某一年的年底將一雄一雌兩只兔子放進圍場中。從第二年一月份開始,這對兔子生下了一雙兒女。此後每一對兔子在第二個月都能產下一對龍鳳胎。一年後,圍場裏能有多少對兔子?
這裏的假定條件是不考慮兔子的近親繁殖問題,也不考慮人類吃貨造成的影響,並且每對兔子都能健康成長……簡單而言就是不考慮科普,只關註數學模型。
遞歸表達
在上一年12月放入一對兔子,次年一月,初始兔子將產下一對新兔子,所以一月份將有兩對兔子。二月份,新兔子還沒有長大,只有初始兔子能夠生產一對兔子,因此二月底將有3對兔子。三月份,初始兔子和1月份誕生的新兔子都將生產一對兔子,再加上二月底本來就有的3對兔子,因此三月底將有2+3=5對兔子:
如果令f(n)表示第n-1月末圍場中的兔子總對數,那麽可以總結出:
可以利用這個關系計算出一年後的兔子總對數,即f(13),這需要知道f(1)到f(12)的值:
一年後圍場中有377對兔子。
程序通常是從0開始的,因此我們可以令f(0) = 1,這就使得f(2)也滿足f(n)的表達式,f(2)=f(1)+f(0),f(0),f(1),f(2),……,f(n)滿足遞歸關系:
這個序列就是著名的斐波那契序列,f(0)和f(1)是序列的初始值。
知道了關系模型後很容易寫出一段“機器咬尾巴”的代碼:
1 # 用遞歸計算斐波那契序列 2 def fabo(n): 3 if n < 2: 4 return 1 5 else: 6 return fabo(n - 1) + fabo(n - 2) 7 8 if __name__ == ‘__main__‘: 9 for i in range(2, 14): 10 print(‘f({0}) = {1}‘.format(i, fabo(i)))
打印結果:
避免無用功
斐波那契的遞歸代碼很優美,它能夠讓人以順序的方式思考,然而這段代碼只能作為遞歸程序的演示樣品,並不能應用於實踐。在運行時便會發現,當輸入大於30時速度會明顯變慢,普通家用機甚至無法計算出f(50)。變慢的關鍵在於每次計算f(n)都要重新求解f(n)前面的所有斐波那契數,相當於做了大量的無用功。
知道癥結後便可以對癥下藥,只要把所有計算過的數存儲起來就能有效改進算法:
1 # 存儲所有計算過的斐波那契數 2 fabo_list = [1, 1] 3 # 用遞歸計算斐波那契序列 4 def fabo_2(n): 5 if n < len(fabo_list): 6 return fabo_list[n] 7 else: 8 fabo_n = fabo_2(n - 1) + fabo_2(n - 2) 9 fabo_list.append(fabo_n) 10 11 return fabo_n 12 13 if __name__ == ‘__main__‘: 14 for i in range(40, 51): 15 print(‘f({0}) = {1}‘.format(i, fabo_2(i)))
全局變量fabo_list將緩存所有計算過的值,這次可以快速計算f(40)~f(50)的值:
用循環代替遞歸
所有的遞歸都可以用循環代替,因此遞歸的實現往往也有對應的循環版本,斐波那契序列也是如此:
1 # 用循環計算斐波那契序列 2 def fabo_3(n): 3 fabo_list = [1] * (n + 1) 4 for i in range(2, n + 1): 5 fabo_list[i] = fabo_list[i - 1] + fabo_list[i - 2] 6 7 return fabo_list[n] 8 9 if __name__ == ‘__main__‘: 10 for i in range(1, 50): 11 print(‘f({0}) = {1}‘.format(i, fabo_3(i)))
這段代碼的運行速度相當快,不僅預存儲了所有計算過的斐波那契數,還比fabo_2省去了因遞歸導致的方法調用的時間。
所有的遞歸都可以用循環代替,反過來也一樣,所有循環也可以用遞歸代替,像Erlang這種編程語言,甚至沒有定義while、for這種用於循環的語法,全部使用遞歸。
斐波那契序列的和
斐波那契序列有很多有趣的性質,其中一個就是求和。
用S(n)表示前n項的和:
先來看一下S(3),利用f(1) = 1,S(3)可以轉換為:
由此可以大膽地推斷:
可以用數學歸納法證明這個結論:
當n=1時,該結論S(1)=f(3)-1=3-1=2,f(0)+f(1)=2,此時結論正確;
假設結論對於n=t-1成立,t是任意自然數:
則當n=t時:
由此可見結論是對的,它可以回答圖中一共繪制了多少對兔子。
求和的代碼相當簡單,不需要傻乎乎的去累加:
1 # 斐波那契序列的前n項之和 2 def fabo_sum(n): 3 return fabo_3(n + 2) - 1
下一篇:遞歸關系的基本解法
作者:我是8位的
出處:http://www.cnblogs.com/bigmonkey
本文以學習、研究和分享為主,如需轉載,請聯系本人,標明作者和出處,非商業用途!
掃描二維碼關註公眾號“我是8位的”
遞歸的邏輯(1)——遞歸關系模型