尾遞迴就是函式在呼叫完自己之後沒有其他操作的遞迴,是遞迴的一種特殊形式。舉個例子,"計算斐波那契數列第 n 項"的遞迴演算法有哪些?
斐波那契數列第 0、1 位都是 1,從第二位開始,每項是前兩位之和,因此用遞迴演算法很容易就能實現出來了:
fun
fib1
(n: int)
: int
這種寫法雖然遞迴呼叫是在方法的最後一行,但其實這裡還有結果相加的操作,並不符合尾遞迴的定義。
簡單遞迴雖然容易理解,但實際上,該演算法會有冗餘計算,比如:fib1(2)會被執行多次,如果 n 越大,這種冗餘計算就會越多:
為了解決上述簡單遞迴實現的弊端,我們可以把已經計算過的結果儲存起來,傳遞給下次計算,所以可以將遞迴寫法進行優化:
fun
fib2
(n: int)
: int
funfibiter
(a: int, b: int, n: int)
: int
else
}
其中,fibiter()
的遞迴**在方法的最後一行,呼叫完也沒有其他的操作,符合尾遞迴的定義。
理論歸理論,我們還是得用實際**來測試一下兩種遞迴演算法的執行耗時情況,這種才更能直**出差別,為了方便測試,這裡寫了乙個耗時測試方法:
fun
timeconsume
(operation:()
-> unit)
ms , end = $
ms , 耗時 $
ms")
}
分別將兩種遞迴演算法丟到耗時測試方法timeconsume()
中,得到測試結果:
fun
main
(args: array
)// 1836311903
// begin = 1612368480299ms , end = 1612368486217ms , 耗時 5918ms
timeconsume
// 1836311903
// begin = 1612368486217ms , end = 1612368486217ms , 耗時 0ms
}
為了拿到斐波那契數列第 45 個元素值,fib1() 耗時近 6s,而 fib2() 耗時 0ms,這是何等的差距。
注意:測試 fib1(50) 會記憶體溢位。雖然上述尾遞迴演算法的耗時很小,但我們知道,遞迴演算法效率其實並不高,因為每遞迴一次就要開闢乙個方法棧,這是有效能消耗的,還有可能因為遞迴次數過多導致出現記憶體溢位的情況,而迭代演算法就沒有這種問題:
fun
fib3
(n: int)
: int
return a
}
同樣的,我們來對尾遞迴演算法和迭代演算法進行耗時測試:
fun
main
(args: array
)// 690383169
// begin = 1612369032575ms , end = 1612369032578ms , 耗時 3ms
timeconsume
// 690383169
// begin = 1612369032578ms , end = 1612369032579ms , 耗時 1ms
}
理論與實際相結合,通過測試結果可以得知,尾遞迴演算法和迭代演算法的差距還是有的,如果電腦 cpu 效能較低,或者方法中存在記憶體操作,這個差距會更大。
注意:因為"計算斐波那契數列第 n 項"這個演算法題目僅僅只是數值執行,對於這 2 個演算法來說太 easy 了,都是毫秒級別的,所以,需要取較後的元素這樣計算量會多一點才能看出差距,同時因為遞迴過多會出現記憶體溢位,因此 n 的取值也不能太大,測試 15000 會記憶體溢位,12000 則不會。既然遞迴有這種缺點,那麼我們以後就杜絕使用遞迴演算法吧?當然不行,遞迴也有乙個很大的優點,那就是**邏輯理解容易,既然這樣,那有沒有辦法讓遞迴演算法的效能跟迭代演算法一樣呢?還真有,kotlin 提供了
tailrec
關鍵字,可以讓尾遞迴演算法
在編譯期自動進行**優化,從而解決尾遞迴演算法的缺點。我們將 fibiter() 加上tailrec
關鍵字:
fun
fib2
(n: int)
: int
// 只加了 tailrec 關鍵字
tailrec
funfibiter
(a: int, b: int, n: int)
: int
再來測試 fib2() 與 fib3() 兩個演算法的耗時情況:
fun
main
(args: array
)// -1256600222
// begin = 1612370134450ms , end = 1612370134451ms , 耗時 1ms
timeconsume
// -1256600222
// begin = 1612370134452ms , end = 1612370134453ms , 耗時 1ms
}
這原本傳入 15000 就會出現記憶體溢位的尾遞迴演算法 fib2(),現在居然能傳入 50000 了,耗時也與迭代演算法 fib3() 一樣,這就是tailrec
關鍵字的厲害之處。
注意:tailrec
關鍵字只能優化尾遞迴演算法,其它遞迴演算法無法優化。Kotlin尾遞迴優化
一 尾遞迴優化 1.遞迴的一種特殊形式 2.呼叫自身後無其他的操作 3.tailrec關鍵字提示編譯器尾遞迴優化 二 具體的來看看一下 說明 package net.println.kotlin.chapter5.tailrecursive author wangdong description 定...
Kotlin尾遞迴優化
尾呼叫 tail call 是函式式程式設計的乙個重要概念,本文介紹它的含義和用法。如果乙個函式中所有遞迴形式的呼叫都出現在函式的末尾,我們稱這個遞迴函式是尾遞迴的。當遞迴呼叫是整個函式體中最後執行的語句且它的返回值不屬於表示式的一部分時,這個遞迴呼叫就是尾遞迴。簡單地說,尾遞迴就是某個函式的最後一...
Kotlin筆記19 尾遞迴優化
前面了我學習了kotlin的遞迴,那麼我還接觸到了kotlin的尾遞迴優化。什麼是尾遞迴優化呢?帶著疑問更好去學習。1.尾遞迴是遞迴的一種特殊形式 2.呼叫自身無其他操作 3.tailrec關鍵字提示編譯器尾遞迴優化 demo中有使用tailrec關鍵字進行提示編譯器尾遞迴優化。尾遞迴演示 實現方式...