-
1 # 淺析架構
-
2 # 思考思考的動物
相對於遞迴,尾遞迴當然是好的,因為尾遞迴在呼叫時,直接覆蓋當前呼叫棧,棧不增長(這稱為 尾遞迴最佳化),但對於迴圈來說就是不見得了。
理論上 尾遞迴和迴圈等價。所以,尾遞迴能實現的需求,迴圈一定可以實現,反之亦然。例如,用歐幾里得輾轉相除法,求取最大公因數(JavaScript),
尾遞迴:
var gcd = (a, b) => b ? gcd(b, a % b) : a;
迴圈:
var gcd = (a, b) => {
}
不過,一般來說,尾遞迴的實現程式碼更精妙,也更為直觀。
對於有些函式式語言,尾遞迴是必不可少不的,例如:Lisp的迴圈語句都是用尾遞迴實現的(scheme)
(define-syntax while
而有些語言,僅僅將尾遞迴提供出來作為特殊情況下使用的工具,本身並不依賴,例如:JavaScript。很多的語言本身根本不支援尾遞迴最佳化,例如:C。
一般來說,支援 λ表示式的語言,大多支援尾遞迴最佳化。
通常情況下,大多數語言在寫正式程式碼中都不建議使用遞迴,不過在寫特殊程式時是允許的,例如:編譯器(裡面的程式處處是直接或間接遞迴)。另外,如果是在學習中 或 可行性分析階段,寫一些練習、原型程式碼,遞迴是不錯的選擇。
對於某些函式式語言,尾遞迴是被鼓勵的,例如:Haskell,
gcd r 0 = r
gcd a b = gcd b (a `mod` b)
這時,要儘量將普通遞迴,化為尾遞迴,絕大多數情況都可以,例如(JavaScript):
階乘:
非尾遞迴:var fact = n => n > 1 ? n * fact(n-1) : 1; 尾遞迴:var fact = (n, total = 1) => n > 1 ? fact(n-1, n * total) : total;斐波那契數列:
非尾遞迴:var fib = n => n > 2 ? fib(n-1) + fib(n-2) : 1;
尾遞迴:var fib = (n, a = 1, b = 1) => n > 2 ? fib(n-1, b, a + b) : b;
最後,不管是否使用遞迴(尾遞迴),我們都需要熟練掌握它,作為一種思維方法而存在。
-
3 # P指向為NULL
貼一個自己寫的針對python語言來實現自定義尾遞迴最佳化。也就是把原來的尾遞迴函式改造成一個生成器,然後再在另一個函數里去迴圈next方法。
回覆列表
概念
遞迴如果層次太多,就會照成棧溢位異常,因為每呼叫一次就會新生成一個棧幀,使用這個棧幀保留當前函式的狀態值。如果沒有必要儲存狀態值,那麼就可以複用棧幀,不會造成棧溢位。
舉例這裡以求n的階乘舉例:
正常遞迴:
假如n=3;那麼每一步都需要保留n值以及下一步函式的返回值,所以每次呼叫都需要建立一個新的棧幀
尾遞迴:
假如n=3,這裡每一次呼叫是可以複用棧幀的,因為不需要儲存狀態值。
總結所以說當遞迴是在當前棧幀執行完之後,不需要再保留當前棧幀,而是帶著當前棧幀的結果,進入到下一棧幀,就可以最佳化為尾遞迴,一般尾遞迴需要滿足遞迴呼叫是函式體中最後執行的語句。比如階乘的例子中最後執行的語句是直接呼叫factorial(n-1, n*result),而不是一個表示式n * factorial(n -1),如果是表示式的話就需要一個棧幀來保留n和factorial(n -1)的結果。