loop、iterate、traversal和recursion這幾個詞是計算機技術書中經常會出現的幾個詞匯。眾所周知,這幾個詞分別翻譯為:循環、迭代、遍歷和遞歸。乍一看,這幾個詞好像都與重復(repeat)有關,但有的又好像不完全是重復的意思。那么這幾個詞到底各是什么含義,有什么區別和聯系呢?下面就試着解釋一下。
- 循環(loop),指的是在滿足條件的情況下,重復執行同一段代碼。比如,while語句。
- 迭代(iterate),指的是按照某種順序逐個訪問列表中的每一項。比如,for語句。
- 遍歷(traversal),指的是按照一定的規則訪問樹形結構中的每個節點,而且每個節點都只訪問一次。
- 遞歸(recursion),指的是一個函數不斷調用自身的行為。比如,以編程方式輸出著名的斐波納契數列。
有了以上定義,這幾個概念之間的區別其實就比較清楚了。至於它們之間的聯系,嚴格來講,它們似乎都屬於算法的范疇。換句話說,它們只不過是解決問題的不同手段和方式,而本質上則都是計算機編程中達成特定目標的途徑。
迭代
迭代算法是用計算機解決問題的一種基本方法。它利用計算機運算速度快、適合做重復性操作的特點,讓計算機對一組指令(或一定步驟)進行重復執行,在每次執行這組指令(或這些步驟)時,都從變量的原值推出它的一個新值。
利用迭代算法解決問題,需要做好以下三個方面的工作:
- 確定迭代變量。在可以用迭代算法解決的問題中,至少存在一個直接或間接地不斷由舊值遞推出新值的變量,這個變量就是迭代變量。
- 建立迭代關系式。所謂迭代關系式,指如何從變量的前一個值推出其下一個值的公式(或關系)。迭代關系式的建立是解決迭代問題的關鍵,通常可以使用遞推或倒推的方法來完成。
- 對迭代過程進行控制。在什么時候結束迭代過程?這是編寫迭代程序必須考慮的問題。不能讓迭代過程無休止地重復執行下去。迭代過程的控制通常可分為兩種情況:一種是所需的迭代次數是個確定的值,可以計算出來;另一種是所需的迭代次數無法確定。對於前一種情況,可以構建一個固定次數的循環來實現對迭代過程的控制;對於后一種情況,需要進一步分析出用來結束迭代過程的條件。
可以用迭代的算法有很經典的問題,比如兔子產子問題:假定你有一雄一雌一對剛出生的兔子,它們在長到一個月大小時開始交配,在第二月結束時,雌兔子產下另一對兔子,過了一個月后它們也開始繁殖,如此這般持續下去。每只雌兔在開始繁殖時每月都產下一對兔子,假定沒有兔子死亡,在一年后總共會有多少對兔子?
還有上樓梯的走法問題:有一段樓梯有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變量記錄狀態,以便回溯。
原理上講,所有遞歸都是可以消除的,代價就是可能自己要維護一個棧。而且我個人認為,很多情況下用遞歸還是必要的,它往往能把復雜問題分解成更為簡單的步驟,而且很能反映問題的本質。
遞歸其實就是利用系統堆棧,實現函數自身調用,或者是相互調用的過程。在通往邊界的過程中,都會把單步地址保存下來,知道等出邊界,再按照先進后出的進行運算,這正如我們裝木桶一樣,每一次都只能把東西方在最上面,而取得時候,先放進取的反而最后取出。遞歸的數據傳送也類似。但是遞歸不能無限的進行下去,必須在一定條件下停止自身調用,因此它的邊界值應是明確的。就向我們裝木桶一樣,我們不能總是無限制的往里裝,必須在一定的時候把東西取出來。比較簡單的遞歸過程是階乘函數,你可以去看一下。但是遞歸的運算方法,往往決定了它的效率很低,因為數據要不斷的進棧出棧。