遞歸(recursion)在計算機科學中是指一種通過重復將問題分解為同類問題的子問題而解決問題的方法。可以極大地減少代碼量。遞歸的能力在於用有限的語句來定義對象的無限集合。遞歸式方法可以被用於解決很多計算機科學問題,因此它是計算機科學中十分重要的一個概念。絕大多數編程語言支持函數的自調用,在這些語言中函數可以通過調用自身來進行遞歸。計算理論可以證明遞歸可以完全取代循環,因此在很多函數編程語言中習慣用遞歸來實現循環。
與重復密切相關的是遞歸,在遞歸技術中,概念是直接或間接由其自身定義的。例如,我們可以通過“表要么為空,要么是一個元素后面再跟上一個表”這樣的描述來定義表。很多編程語言都支持遞歸。在C語言中,函數F是可以調用自身的,既可以從F的函數體中直接調用自己,也可以通過一連串的函數調用,最終間接調用F。另一個重要思想——歸納,是與“遞歸”密切相關的,而且常用於數學證明中。
使用遞歸要注意的有兩點:
1)遞歸就是在過程或函數里調用自身;
2)在使用遞歸時,必須有一個明確的遞歸結束條件,稱為遞歸出口。
遞歸分為兩個階段:
1)遞推:把復雜的問題的求解推到比原問題簡單一些的問題的求解;
2)回歸:當獲得最簡單的情況后,逐步返回,依次得到發雜的解。
斐波那契數列
1 int fib(int n) 2 3 { 4 5 if(0 == n) 6 7 return 0; 8 9 if(1 == n) 10 11 return 1; 12 13 if(n > 1) 14 15 return fib(n-1)+fib(n-2); 16 17 }
上面就是一個簡單的遞歸調用了,由於遞歸引起一系列的函數調用,並且有可能會有一系列的重復計算,遞歸算法的執行效率相對較低。
遞歸調用實際上是函數自己在調用自己,而函數的調用開銷是很大的,系統要為每次函數調用分配存儲空間,並將調用點壓棧予以記錄。而在函數調用結束后,還要釋放空間,彈棧恢復斷點。所以說,函數調用不僅僅浪費空間,還浪費時間。
迭代(interation)是程序中對一組指令(或一定步驟)的重復。它即可以用作通用的術語(與“重復”同義),也可以用來描述一種特定形式的具有可變狀態的重復。
計算機的威力源自其反復執行同一任務或同一任務不同版本的能力。在計算領域,迭代這一主題會以多種形式出現。數據模型中的很多概念(比如表)都是某種形式的重復,比如“表要么為空,要么由一個元素接一個元素,再接一個元素,如此往復而成”。使用迭代,程序和算法可以在不需要單獨指定大量相似步驟的情況下,執行重復性的任務,如“執行下一步驟1000次”。編程語言使用像C語言中的while
語句和for
語句那樣的循環結構,來實現迭代算法。
相比迭代,用遞歸解決這些問題來的更輕松,別人理解起你的代碼也更加容易。但是遞歸有它自身的問題,每一次遞歸基本都需要在棧上申請一塊新的空間,如果你干得漂亮的話用一個遞歸爆掉一個棧也不是很難的事情,除此之外,個人認為遞歸相對於迭代來說和計算機本身的設計原理有些不搭,同樣的功能遞歸應該要慢一些。
有一種計算階乘的方式,這里使用遞歸函數定義了計算階乘的函數:
1 func factorial(n: Int) -> Int { 2 if n == 0 { 3 return 1 4 } 5 return n * factorial(n - 1) 6 }
現在我們試着描述這個函數的計算過程,以factorial(5)為例,一步步代換其計算過程。我們可以看到一個先逐步展開而后收縮的形狀。在展開階段里,這一計算過程構造起一個推遲進行的操作所形成的鏈條(在這里是一個乘法的鏈條),收縮過程表現為這些運算的實際執行。其形狀可以描繪為如下的圖例:
1 (factorial 5) 2 (5 * (factorial 4)) 3 (5 * (4 * (factorial 3)) 4 (5 * (4 * (3 * (factorial 2)) 5 (5 * (4 * (3 * (2 * (factorial 1))) 6 (5 * (4 * (3 * (2 * 1)))) 7 (5 * (4 * (3 * 2))) 8 (5 * (4 * 6)) 9 (5 * 24) 10 120
這樣的計算過程是一個遞歸計算過程。遞歸計算過程由一個推遲執行的運算鏈條刻畫,要執行遞歸計算過程,解釋器就需要維護好那些以后要執行的操作的軌跡。
這種不同對於計算機而言卻是重要的。在迭代的情況里,計算過程的任何一點,固定數目的狀態變量都提供了有關計算狀態的一個完整描述。而描述一個遞歸計算過程,需要一些“隱含”信息,它們並未保存在程序變量里,而是由解釋器維持着,指明了在所推遲的運算所形成的鏈條里,計算過程正處於何處(這種解釋器維持運算鏈條,需要使用一種稱為棧的數據結構)。這個鏈條越長,需要保存的信息也就越多。
遞歸計算過程,通常容易理解,符合人類的思維習慣。但由於需要使用棧機制實現,其空間復雜度通常很高。對於一些遞歸層數深的計算,計算機會力不從心,空間上會以內存崩潰而告終。而且遞歸也帶來了大量的函數調用,這也有許多額外的時間開銷。所以在深度大時,它的時間復雜度和空間復雜度就都不好了。
迭代算法是用計算機解決問題的一種基本方法。它利用計算機運算速度快,適合做重復性操作的特點,讓計算機對一組命令(或一定步驟)進行重復執行,在每次執行這組命令(或步驟)時,都從變量的原值退出它的一個新值。利用迭代算法解決問題,需要做好以下三個方面的工作:
(1)確定迭代變量。在可以用迭代算法解決的問題中,至少存在一個直接或間接地不斷由舊值遞推出新值的變量,這個變量就是迭代變量。
(2)建立迭代關系。所謂迭代關系,指如何從變量的前一個值推出其下一個值的公式(或關系)。迭代關系式的建立是解決問題的關鍵,通常可以使用遞推或倒推的方法來完成。
(3)對迭代過程進行控制。在什么時候結束迭代過程?這是編寫迭代程序必須考慮的問題。不能讓迭代過程無休止地重復執行下去。迭代過程的控制通常可分為兩種情況:一種是所需的迭代次數是個確定的值,可以計算出來;另一種是所需的迭代次數無法確定。對於前一種情況,可以構建一個固定次數的循環來實現對迭代過程的控制;對於后一種情況,需要進一步分析出用來結束迭代過程的條件。
遞歸是設計和描述算法的一種有力的工具,能采用遞歸描述的算法通常有這樣的特征:為求解規模為N的問題,設法將它分解成規模較小的問題,然后從這些小問題的解方便地構造出大問題的解,並且這些規模較小的問題也能采用同樣的分解和綜合方法,分解成規模更小的問題,並從這些更小問題的解構造出規模較大問題的解。特別地,當規模N=1時,能直接得解。
遞歸算法的執行過程分遞推和回歸兩個階段。在遞推階段,把較復雜的問題(規模為n)的求解推到比原問題簡單一些的問題(規模小於n)的求解。例如上例中,求解fib(n),把它推到求解fib(n-1)和fib(n-2)。也就是說,為計算fib(n),必須先計算fib(n-1)和fib(n- 2),而計算fib(n-1)和fib(n-2),又必須先計算fib(n-3)和fib(n-4)。依次類推,直至計算fib(1)和fib(0),分別能立即得到結果1和0。在遞推階段,必須要有終止遞歸的情況。例如在函數fib中,當n為1和0的情況。
在回歸階段,當獲得最簡單情況的解后,逐級返回,依次得到稍復雜問題的解,例如得到fib(1)和fib(0)后,返回得到fib(2)的結果,……,在得到了fib(n-1)和fib(n-2)的結果后,返回得到fib(n)的結果。
在編寫遞歸函數時要注意,函數中的局部變量和參數知識局限於當前調用層,當遞推進入“簡單問題”層時,原來層次上的參數和局部變量便被隱蔽起來。在一系列“簡單問題”層,它們各有自己的參數和局部變量。
由於遞歸引起一系列的函數調用,並且可能會有一系列的重復計算,遞歸算法的執行效率相對較低。當某個遞歸算法能較方便地轉換成遞推算法時,通常按遞推算法編寫程序。例如上例計算斐波那契數列的第n項的函數fib(n)應采用遞推算法,即從斐波那契數列的前兩項出發,逐次由前兩項計算出下一項,直至計算出要求的第n項。
參考
http://note.zqguo.com/archives/301
http://www.ituring.com.cn/tupubarticle/5504#
http://lincode.github.io/Recursion-Iteration/
http://www.bianceng.cn/Programming/sjjg/200901/11200.htm