1. 程式人生 > >漫談遞迴:迴圈與迭代

漫談遞迴:迴圈與迭代

漫談遞迴:迴圈與迭代

 理清遞迴、迭代、迴圈的概念

感謝  參考或原文  

先摘抄“為之漫筆”對這幾個概念的一段理解

loop、iterate、traversal和recursion這幾個詞是計算機技術書中經常會出現的幾個詞彙。眾所周知,這幾個詞分別翻譯為:迴圈、迭代、遍歷和遞迴。乍一看,這幾個詞好像都與重複(repeat)有關,但有的又好像不完全是重複的意思。那麼這幾個詞到底各是什麼含義,有什麼區別和聯絡呢?下面就試著解釋一下。

  • 迴圈(loop),指的是在滿足條件的情況下,重複執行同一段程式碼。比如,while語句。
  • 迭代(iterate),指的是按照某種順序逐個訪問列表中的每一項。比如,for語句。
  • 遍歷(traversal),指的是按照一定的規則訪問樹形結構中的每個節點,而且每個節點都只訪問一次。
  • 遞迴(recursion),指的是一個函式不斷呼叫自身的行為。比如,以程式設計方式輸出著名的斐波納契數列。

有了以上定義,這幾個概念之間的區別其實就比較清楚了。至於它們之間的聯絡,嚴格來講,它們似乎都屬於演算法的範疇。換句話說,它們只不過是解決問題的不同手段和方式,而本質上則都是計算機程式設計中達成特定目標的途徑。

迭代

迭代演算法是用計算機解決問題的一種基本方法。它利用計算機運算速度快、適合做重複性操作的特點,讓計算機對一組指令(或一定步驟)進行重複執行,在每次執行這組指令(或這些步驟)時,都從變數的原值推出它的一個新值。

利用迭代演算法解決問題,需要做好以下三個方面的工作:

  1. 確定迭代變數。在可以用迭代演算法解決的問題中,至少存在一個直接或間接地不斷由舊值遞推出新值的變數,這個變數就是迭代變數。
  2. 建立迭代關係式。所謂迭代關係式,指如何從變數的前一個值推出其下一個值的公式(或關係)。迭代關係式的建立是解決迭代問題的關鍵,通常可以使用遞推或倒推的方法來完成。
  3. 對迭代過程進行控制。在什麼時候結束迭代過程?這是編寫迭代程式必須考慮的問題。不能讓迭代過程無休止地重複執行下去。迭代過程的控制通常可分為兩種情況:一種是所需的迭代次數是個確定的值,可以計算出來;另一種是所需的迭代次數無法確定。對於前一種情況,可以構建一個固定次數的迴圈來實現對迭代過程的控制;對於後一種情況,需要進一步分析出用來結束迭代過程的條件。

可以用迭代的演算法有很經典的問題,比如兔子產子問題:假定你有一雄一雌一對剛出生的兔子,它們在長到一個月大小時開始交配,在第二月結束時,雌兔子產下另一對兔子,過了一個月後它們也開始繁殖,如此這般持續下去。每隻雌兔在開始繁殖時每月都產下一對兔子,假定沒有兔子死亡,在一年後總共會有多少對兔子?

還有上樓梯的走法問題:有一段樓梯有10級臺階,規定每一步只能跨一級或兩級,要登上第10級臺階有幾種不同的走法?

這兩個問題可以參看以前寫的一篇文章:趣味演算法之兔子產子問題

迭代與迴圈

先從字面上看:

  • 迭代:“迭”:輪流,輪番,替換,交替,更換。“代”:代替。所以迭代的意思是:變化的迴圈,這種變化就是輪番代替,輪流代替。
  • 迴圈:不變的重複。

個人認為迭代是迴圈的一種,迴圈體程式碼分為固定迴圈體,和變化的迴圈體。

固定的迴圈舉例:

1 for($i=0; $i < 8; $i++){
2     echo 'Welcome to NowaMagic';
3 }

實現迭代:

1 $sum = 0;
2  
3 for($i = 1; $i <= 1000; $i++ ){
4     $sum $sum + i;
5 }

上面的迭代是常見的遞增式迭代。類似的還有遞減式迭代,遞乘式迭代。

迭代的好處:迭代減少了冗餘程式碼,提高了程式碼的利用率和動態性。

迴圈、迭代與遞迴

1. 遞迴演算法與迭代演算法的設計思路區別在於:函式或演算法是否具備收斂性,當且僅當一個演算法存在預期的收斂效果時,採用遞迴演算法才是可行的,否則,就不能使用遞迴演算法。

當然,從理論上說,所有的遞迴函式都可以轉換為迭代函式,反之亦然,然而代價通常都是比較高的。但從演算法結構來說,遞迴宣告的結構並不總能夠轉換為迭代結構,原因在於結構的引申本身屬於遞迴的概念,用迭代的方法在設計初期根本無法實現,這就像動多型的東西並不總是可以用靜多型的方法實現一樣。這也是為什麼在結構設計時,通常採用遞迴的方式而不是採用迭代的方式的原因,一個極典型的例子類似於連結串列,使用遞迴定義及其簡單,但對於記憶體定義(陣列方式)其定義及呼叫處理說明就變得很晦澀,尤其是在遇到環鏈、圖、網格等問題時,使用迭代方式從描述到實現上都變得很不現實。

2. 遞迴其實是方便了程式設計師難為了機器。它只要得到數學公式就能很方便的寫出程式。優點就是易理解,容易程式設計。但遞迴是用棧機制實現的,每深入一層,都要佔去一塊棧資料區域,對巢狀層數深的一些演算法,遞迴會力不從心,空間上會以記憶體崩潰而告終,而且遞迴也帶來了大量的函式呼叫,這也有許多額外的時間開銷。所以在深度大時,它的時空性就不好了。

迴圈其缺點就是不容易理解,編寫複雜問題時困難。優點是效率高。執行時間只因迴圈次數增加而增加,沒什麼額外開銷。空間上沒有什麼增加。

3. 區域性變數佔用的記憶體是一次性的,也就是O(1)的空間複雜度,而對於遞迴(不考慮尾遞迴優化的情況),每次函式呼叫都要壓棧,那麼空間複雜度是O(n),和遞迴次數呈線性關係。

4. 遞迴程式改用迴圈實現的話,一般都是要自己維護一個棧的,以便狀態的回溯。如果某個遞迴程式改用迴圈的時候根本就不需要維護棧,那其實這個遞迴程式這樣寫只是意義明顯一些,不一定要寫成遞迴形式。但很多遞迴程式就是為了利用函式自身在系統棧上的auto變數記錄狀態,以便回溯。

原理上講,所有遞迴都是可以消除的,代價就是可能自己要維護一個棧。而且我個人認為,很多情況下用遞迴還是必要的,它往往能把複雜問題分解成更為簡單的步驟,而且很能反映問題的本質。

遞迴其實就是利用系統堆疊,實現函式自身呼叫,或者是相互呼叫的過程。在通往邊界的過程中,都會把單步地址儲存下來,知道等出邊界,再按照先進後出的進行運算,這正如我們裝木桶一樣,每一次都只能把東西方在最上面,而取得時候,先放進取的反而最後取出。遞迴的資料傳送也類似。但是遞迴不能無限的進行下去,必須在一定條件下停止自身呼叫,因此它的邊界值應是明確的。就向我們裝木桶一樣,我們不能總是無限制的往裡裝,必須在一定的時候把東西取出來。比較簡單的遞迴過程是階乘函式,你可以去看一下。但是遞迴的運算方法,往往決定了它的效率很低,因為資料要不斷的進棧出棧。

但是遞迴作為比較基礎的演算法,它的作用不能忽視。