分享自:酷勤網 www.kuqin.com
遞迴函式
遞迴可以描述不同的概念,如果說乙個函式是遞迴的,那麼就是說函式的定義中(直接或者間接地)引用了該函式本身。
比如求斐波那契數列,使用swift實現:
從函式定義看,fib(n)的計算呼叫了fib(n-1)和f(n-2)。所以,fib是乙個遞迴函式。func fib(n: int) -> int
return fib(n - 1) + fib(n - 2)
}
尾遞迴遞迴函式中有一類叫尾遞迴
,比較特殊,可以單獨說一下。在說尾遞迴之前,可以先了解一下尾呼叫
。尾呼叫是指乙個函式的最後乙個動作是乙個函式呼叫的情形:即這個呼叫的返回值直接被當前函式返回。這種情形下稱該呼叫位置為尾位置。若這個函式在尾位置呼叫本身(或是乙個尾呼叫本函式的其他函式等等),則稱這種情況為尾遞迴。
尾遞迴是遞迴的一種特殊情形。尾呼叫不一定是遞迴呼叫。
func f(n: int) -> int
形如以上**的函式就是尾呼叫。當呼叫變為呼叫自身時,尾呼叫就變成了尾遞迴。舉乙個求階乘的尾遞迴的例子:
尾遞迴優化func factorial(n: int, total: int) -> int
return factorial(n - 1, total: n * total)
}
函式呼叫是有開銷的。當乙個函式呼叫發生時,計算機必須「記住」呼叫函式的位置 — 返回位置,才可以在呼叫結束時帶著返回值回到原位置,以繼續呼叫前的計算。返回位置一般會存放在呼叫棧上。
但在尾呼叫的情況中,計算機不需要記住尾呼叫的位置,因為該位置已經是函式的尾部,就算是儲存了遞迴呼叫的現場,當從遞迴呼叫返回時,除了再次返回,也已無其他事可做了。所以,儲存方法之前積累的各種狀態對於遞迴呼叫結果已經沒有任何意義了,因此完全可以把本次方法中留在堆疊中的資料清除,把空間讓給最後的遞迴呼叫。這樣的優化使得遞迴不會在呼叫堆疊上產生堆積,意味著即使是「無限」遞迴也不會讓堆疊溢位。這就是尾遞迴優化的作用。
對函式呼叫在尾位置的遞迴的函式,由於函式自身呼叫次數很多,遞迴層級很深,尾遞迴優化則使原本 o(n) 的呼叫棧空間,變為只需要 o(1) 的空間。因此一些程式語言的標準要求語言實現進行尾呼叫消除,這樣可以大大提公升尾遞迴的效率。
計算過程
遞迴計算過程
我們以階乘的計算為例說明計算過程所呈現的不同「形狀」。
有一種計算階乘的方式,這裡使用遞迴函式定義了計算階乘的函式:
現在我們試著描述這個函式的計算過程,以factorial(5)為例,一步步代換其計算過程。我們可以看到乙個先逐步展開而後收縮的形狀。在展開階段裡,這一計算過程構造起乙個func factorial(n: int) -> int
return n * factorial(n - 1)
}
推遲進行的操作所形成的鏈條(在這裡是乙個乘法的鏈條),收縮過程表現為這些運算的實際執行。其形狀可以描繪為如下的圖例:
這樣的計算過程是乙個遞迴計算過程。遞迴計算過程由乙個推遲執行的運算鏈條刻畫,要執行遞迴計算過程,直譯器就需要維護好那些以後要執行的操作的軌跡。(factorial 5)
(5 * (factorial 4))
(5 * (4 * (factorial 3))
(5 * (4 * (3 * (factorial 2))
(5 * (4 * (3 * (2 * (factorial 1)))
(5 * (4 * (3 * (2 * 1))))
(5 * (4 * (3 * 2)))
(5 * (4 * 6))
(5 * 24)
120
迭代計算過程
然後我們再使用另外一種觀點來計算階乘。我們可以得到如下函式:
當我們也以factorial(5)為例,一步步代換其計算過程時。我們能發現其呈現如下形式:func fact-iter(n: int, total: int) -> int
return fact-iter(n - 1, total: n * total)
}
(factorial 5)
(fact-iter 1 5)
(fact-iter 5 4)
(fact-iter 20 3)
(fact-iter 60 2)
(fact-iter 120 1)
120
這個過程的形態並沒有任何增長或者收縮的勢態。計算過程的軌跡都儲存在幾個固定數目的狀態變數中。一般來說,迭代計算過程是那種狀態可以用固定數目的變數描述的計算過程;而與此同時,又有一套固定的規則,描述計算過程在才乙個狀態到下乙個狀態的轉換規則;還需要乙個結束檢查,描述計算過程終止的條件。
從函式角度來看,factorial
和fact-iter
都函式是遞迴函式,進一步說,fact-iter
是乙個尾遞迴函式。我們可以發現尾遞迴函式都是一種迭代計算過程。
對比上面描述的兩種計算過程從結果上來看並沒有太多差異:兩者計算的都是同乙個定義域裡的同乙個數學函式,都需要使用與n成正比的步驟去計算出結果。確實,這兩個計算過程甚至採用了同樣的乘運算序列,得到了相同的部分乘積序列。但在另一方面,如果我們考慮這兩個計算的「形狀」,就會發現他們之間的不同。
這種不同對於計算機而言卻是重要的。在迭代的情況裡,計算過程的任何一點,固定數目的狀態變數都提供了有關計算狀態的乙個完整描述。而描述乙個遞迴計算過程,需要一些「隱含」資訊,它們並未儲存在程式變數裡,而是由直譯器維持著,指明了在所推遲的運算所形成的鏈條裡,計算過程正處於何處(這種直譯器維持運算鏈條,需要使用一種稱為棧的資料結構)。這個鏈條越長,需要儲存的資訊也就越多。
這也是尾遞迴優化存在的意義。通常遞迴計算過程可能遞迴很多步,這就意味著這上文所描述的運算鏈條會非常長。通常同一時刻計算機的資源是有限的,那麼去除這種大規模資源的消耗將會有很多幫助。
從理論上說,所有的遞迴計算過程都可以轉換為迭代計算過程。反之亦然,然而代價通常都是比較高的。但從演算法結構來說,遞迴宣告的結構並不總能夠轉換為迭代結構,原因在於結構的引申本身屬於遞迴的概念,用迭代的方法在設計初期根本無法實現。
遞迴計算過程,通常容易理解,符合人類的思維習慣。但由於需要使用棧機制實現,其空間複雜度通常很高。對於一些遞迴層數深的計算,計算機會力不從心,空間上會以記憶體崩潰而告終。而且遞迴也帶來了大量的函式呼叫,這也有許多額外的時間開銷。所以在深度大時,它的時間複雜度和空間複雜度就都不好了。
遞迴和迭代 迭代與遞迴
很多程式設計小白都會遇到 迭代 和 遞迴 的問題 包括我自己 大部分同學還是不知道迭代與遞迴的區別。下面我就嘗試用最通俗易懂的模式講解遞迴與迭代的區別。1.迭代 迭代其實很簡單,我們在程式設計中經常用到迭代。比如說 i 1 print i 這個就是乙個迭代,沒想到吧。迭代的意思其實就是在迴圈 現了參...
遞迴和迭代
遞迴和迭代是兩種常用的演算法,很多人知道怎麼寫遞迴和迭代,但是不知道什麼時候該用遞迴,什麼時候該用迭代。下面的 分別通過使用遞迴和迭代計算fibonacci數列,可以很清楚的看到效率的驚人差別。當然,很難有個準則說什麼時候該用遞迴,什麼時候該用迭代,但有乙個很簡單的判斷方法 如果你的遞迴呼叫是在函式...
遞迴和迭代
遞迴和迭代都是迴圈中的一種。簡單地說,遞迴是重複呼叫函式自身實現迴圈。迭代是函式內某段 實現迴圈,而迭代與普通迴圈的區別是 迴圈 中參與運算的變數同時是儲存結果的變數,當前儲存的結果作為下一次迴圈計算的初始值。遞迴迴圈中,遇到滿足終止條件的情況時逐層返回來結束。迭代則使用計數器結束迴圈。當然很多情況...