漫談遞歸和迭代

 

遞歸(recursion)在計算機科學中是指一種經過重複將問題分解爲同類問題的子問題而解決問題的方法。能夠極大地減小代碼量。遞歸的能力在於用有限的語句來定義對象的無限集合。遞歸式方法能夠被用於解決不少計算機科學問題,所以它是計算機科學中十分重要的一個概念。絕大多數編程語言支持函數的自調用,在這些語言中函數能夠經過調用自身來進行遞歸。計算理論能夠證實遞歸能夠徹底取代循環,所以在不少函數編程語言中習慣用遞歸來實現循環。git

與重複密切相關的是遞歸,在遞歸技術中,概念是直接或間接由其自身定義的。例如,咱們能夠經過「表要麼爲空,要麼是一個元素後面再跟上一個表」這樣的描述來定義表。不少編程語言都支持遞歸。在C語言中,函數F是能夠調用自身的,既能夠從F的函數體中直接調用本身,也能夠經過一連串的函數調用,最終間接調用F。另外一個重要思想——概括,是與「遞歸」密切相關的,並且經常使用於數學證實中。github

使用遞歸要注意的有兩點:算法

1)遞歸就是在過程或函數裏調用自身;編程

2)在使用遞歸時,必須有一個明確的遞歸結束條件,稱爲遞歸出口。數據結構

遞歸分爲兩個階段:編程語言

1)遞推:把複雜的問題的求解推到比原問題簡單一些的問題的求解;函數

2)迴歸:當得到最簡單的狀況後,逐步返回,依次獲得發雜的解。工具

斐波那契數列spa

 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

相關文章
相關標籤/搜索