迭代 與 遞歸 的選擇

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

歸與迭代都是基於控制結構:迭代用重複結構,而遞歸用選擇結構。遞歸與迭代都涉及重複:迭代顯式使用重複結構,而遞歸通過重複函數調用實現重複。遞歸與迭代都涉及終止測試:迭代在循環條件失敗時終止,遞歸在遇到基本情況時終止。使用計數器控制重複的迭代和遞歸都逐漸到達終止點:迭代一直修改計數器,直到計數器值使循環條件失敗;遞歸不斷產生最初問題的簡化副本,直到達到基本情況。迭代和遞歸過程都可以無限進行:如果循環條件測試永遠不變成false,則迭代發生無限循環;如果遞歸永遠無法回推到基本情況,則發生無窮遞歸。

遞歸有許多缺點,它重複調用機制,因此重複函數調用的開銷很大,將佔用很長的處理器時間和大量的內存空間。每次遞歸調用都要生成函數的另一個副本(實際上只是函數變量的另一個副本).從而消耗大量內存空間。迭代通常發生在函數內,因此沒有重複調用函數和多餘內存賦值的開銷。那麼,爲什麼選擇遞歸呢?

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

摘要:在算法的分析與設計中,遞歸和迭代都是特別有力的工具,很多難解的問題都是通過遞歸或迭代算法解出來的。本文在比較這兩種算法在不同情況下的可行性的基礎上,闡述了怎樣對這兩種算法進行有效的選擇。 
關鍵詞:遞歸算法 迭代算法 程序 
0 引言
在算法分析與設計中,遞歸與迭代是我們解決循環問題常用的兩種方法。那麼,在既可以用遞歸算法又可以用迭代算法解決的問題中,我們究竟該選用哪種算法呢?在程序設計中,我們不但講求代碼所能實現的功能,而且在實現相同功能的同時,更注重優化代碼、提高代碼的執行效率。這也是我們在選擇遞歸還是迭代思想時考慮的主要因素。
1 遞歸和迭代概述
如果一個問題剛開始難以解決,可以將其簡化後再嘗試解決。如果這個過程可以重複進行,問題最終會變得容易處理。由此引出兩種不同的方法:遞歸和迭代。循環或迭代,是一種重複執行一個過程的方法;遞歸是另一種方法。遞歸函數是通過調用函數自身來完成任務,而且在每次調用自身時減少任務量。而迭代是循環的一種形式,這種循環不是由用戶輸入而控制,每次迭代步驟都必須將剩餘的任務減少;也就是說,循環的每一步都必須執行一個有限的過程,並留下較少的步驟。循環的進度通常用一個在每次迭代時都進行自增或自減的變量的值來衡量,直到到達預定的目標爲止。用遞歸算法表示許多問題的求解方法時算法思想非常簡潔。但是遞歸算法不僅時間效率非常差,而且由於遞歸算法是不斷的函數調用和函數返回過程,因此其實際的計算機運行時間通常遠大於循環方式算法的計算機運行時間,甚至在有限的時間內無法求解。這就存在一個把遞歸算法化爲非遞歸算法的問題。
2 需要用迭代消解遞歸的情況 
遞歸算法特別適合於所研究的問題或所處理的數據本身是遞歸定義的情況。然而,並不意味着這種遞歸定義保證遞歸算法是解決該問題的最好方法。事實上,主要是因爲拿那種不合適的例子來解釋遞歸算法概念,從而造成了對程序設計中使用遞歸的普遍懷疑和否定態度,並把遞歸同低效等同起來。而且在遞歸算法中,往往會因爲追求代碼短或者在求解問題時一味追求規律性,多用了無用的壓棧和出棧的操作。比如用循環消解的尾遞歸,是多了無用的壓棧和出棧才使速度受損的;斐波那契數列計算的遞歸改循環迭代所帶來的速度大幅提升,是因爲改掉了重複計算的毛病。假使一個遞歸過程中本身包含了大量冗餘的操作,並且這個過程又可以用迭代來達到相同的效果。這時,我們就一般用迭代來消解遞歸。也就是說尾遞歸算法和單向遞歸算法可用迭代算法來代替。可以用一個方案來描述人們力圖在其中避免使用算法遞歸的程序,這個方案展示了其構成的模型。(1)式或等價的(2)式就是這個方案:
P≡ if B then (S;P)          (1)
P≡(S; if B then P)          (2)
要計算用簡單遞歸關係定義的值時,這種方案是很自然的。如下例:
Function F (I: integer ):integer;
Begin if I>0 then F:=I*F(I-1)
      Else F:=1;
End                                    (3)
很明顯,在這種情況下,遞歸可由簡單迭代代替,即由下面的程序代替。
I:=0;F:=1;
While I<n do
Begin I:=I+1;F:=I*F
End (4)
一般地,對應於方案(1)或(2)的程序應按照下列方案改寫:
P ≡(x:=x0;while B do S)
 還有更復雜的遞歸構造方案,這些方案可以並且應該改寫成迭代計算的形式。一個例子就是Fibonacci 數的計算,這些數是按遞歸關係定義的:
 FIBn+1=FIBn+FIBn-1對n>0
 而FIB1=1,FIB0=0,一個直接的自然的解法導致程序
 function fib (n: integer): integer;
 begin if n=0 then fib :=0else
 if n=1 then fib :=1 else 
 fib:=fib(n-1)+fib(n-2)
 end
以調用fib(n)來計算FIBn ,就引起這個函數過程的遞歸活動。頻繁程度如何呢?我們注意到,對於n>1,每調用一次引起兩個新的調。因此,調用的總次數按指數增長,如下圖所示fib(5)的遞歸樹。這樣一個程序顯然是不實用的。
但顯見,利用輔助變量x=FIBi與y=FIBi-1,就可以用迭代方案來計算Fibonicca 數,而避免同一值的重複計算。
 { 對n>0計算x=FIBn}
i:=1;x:=1;y:=0;
while i<n do
    begin z:=x; i:=i+1;
         x:=x + y; y:=z
 end
(注意,對x, y, z的三個賦值,可以只表示成兩個賦值,而不需要輔助變量z :x:=x +y ; y:=x-y).由上述例子可以看出,當存在明顯的迭代解法時要避免使用遞歸。
3 不需要消解的遞歸
那種盲目的消解遞歸,不惜一切代價躲避遞歸,認爲“遞歸的速度慢,爲了提高速度,必須用棧或者其他的方法來消解”的說法是很片面的。如果一個遞歸過程用非遞歸的方法實現後,速度提高了,那只是因爲遞歸做了一些無用功。假使一個遞歸過程必須要用棧才能消解,那麼完全模擬後的結果根本就不會對速度有任何提升,只會減慢;如果你改完後速度提升了,那隻證明你的遞歸函數寫的有問題,如多了許多重複操作——打開關閉文件、連接斷開數據庫,而這些完全可以放到遞歸外面。可以在本質上是非遞歸的機器上實現遞歸過程這一事實本身就證明:爲着實際目的,每一個遞歸程序都可以翻譯成純粹迭代的形式,但這包含着對遞歸棧的顯式處理,而這些運算常常模糊了程序的本質,以致使它非常難以理解。 
因此,是遞歸的而不是迭代的算法應當表述成遞歸過程。如漢諾塔問題等。漢諾塔問題的遞歸算法中有兩處遞歸調用,並且其中一處遞歸調用語句後還有其他語句,因此該遞歸算法不是尾遞歸或單向遞歸。要把這樣的遞歸算法轉化爲非遞歸算法,並沒有提高程序運行的速度,反而會使程序變得複雜難懂,這是不可取的。也就是說,很多遞歸算法並不容易改寫成迭代程序:它們本質上是遞歸的,沒有簡單的迭代形式。這樣的遞歸算法不宜轉化爲非遞歸算法。
4 結束語
說到底,在我們選擇算法時應該全面分析算法的可行性、效率、代碼優化。在綜合了算法的各個因素後,選擇合適的算法來編寫程序,這樣的程序纔會達到優化的效果

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章