尾呼叫(tail call)是函式式程式設計的乙個重要概念,本文介紹它的含義和用法。
如果乙個函式中所有遞迴形式的呼叫都出現在函式的末尾,我們稱這個遞迴函式是尾遞迴的。當遞迴呼叫是整個函式體中最後執行的語句且它的返回值不屬於表示式的一部分時,這個遞迴呼叫就是尾遞迴。
簡單地說,尾遞迴就是某個函式的最後一步是呼叫另乙個函式,且在這一步中,除了呼叫函式外,前後沒有其他操作。
因此,尾遞迴就是遞迴的一種特殊形式。
//尾呼叫
funtailcall
(n : int)
: int
//不是尾呼叫
funnottailcall
(n : int)
: int
//不是尾呼叫
funnottailcalleither
(n : int)
:int
//這兩種函式呼叫不是尾呼叫, 是因為它們在最後一步除了呼叫函式之外還有其它操作
要注意的是,尾呼叫不一定出現在函式尾部,只要是最後一步操作即可
//尾呼叫
funcumsum
(n:int, res:long)
:long =
if(n ==
1) n+res else
cumsum
(n -
1, n+res)
//尾呼叫
funcumsum
(n:int, res:long)
:long
//這些函式結束前的最後一步都只有函式呼叫,因此是尾呼叫
我們通常利用編譯器進行的尾呼叫優化來改善**效能,解決因遞迴次數過多造成的棧溢位異常stackoverflowerror
尾呼叫之所以與其他呼叫不同,就在於它的特殊的呼叫位置。
我們知道,函式呼叫會在記憶體形成乙個"呼叫記錄",又稱"呼叫幀"(call frame),儲存呼叫位置和內部變數等資訊。如果在函式a的內部呼叫函式b,那麼在a的呼叫記錄上方,還會形成乙個b的呼叫記錄。等到b執行結束,將結果返回到a,b的呼叫記錄才會消失。如果函式b內部還呼叫函式c,那就還有乙個c的呼叫記錄棧,以此類推。所有的呼叫記錄,就形成乙個"呼叫棧"(call stack)。
尾呼叫由於是函式的最後一步操作,所以不需要保留外層函式的呼叫記錄,因為呼叫位置、內部變數等資訊都不會再用到了,只要直接用內層函式的呼叫記錄,取代外層函式的呼叫記錄就可以了。
funf(
)f()
;// 等同於
funf()
f();
// 等同於g(3);
上面**中,如果函式g不是尾呼叫,函式f就需要儲存內部變數m和n的值、g的呼叫位置等資訊。但由於呼叫g之後,函式f就結束了,所以執行到最後一步,完全可以刪除 f() 的呼叫記錄,只保留 g(3) 的呼叫記錄。
這就叫做"尾呼叫優化"(tail call optimization),即只保留內層函式的呼叫記錄。如果所有函式都是尾呼叫,那麼完全可以做到每次執行時,呼叫記錄只有一項,這將大大節省記憶體。這就是"尾呼叫優化"的意義。
有示例累加函式,計算從1到n之和
fun
cumsum
(n:int)
:long =
if(n ==1)
1else n+
cumsum
(n-1
)val res =
cumsum
(n)
當輸入的n數值過大時,會造成棧溢位異常stackoverflowerror:
因此,我們需要對這樣的遞迴函式改寫為尾遞迴函式,確保最後一步只呼叫自身
因此我們可以得到以下**:
fun
cumsum
(n:int, res:long)
:long =
if(n ==
1) n+res else
cumsum
(n -
1, n+res)
var res =
0res =
cumsum
(n, res)
這樣,我們完成了從普通遞迴函式到尾遞迴函式的改寫,但這樣的改寫降低了可讀性,其他人在函式使用時會造成困惑:為什麼計算n的累加和需要傳入n和0呢?
要解決**的使用問題,我們可以採取兩種辦法:
在尾遞迴函式外提供乙個正常形式的函式:
//尾遞迴函式計算累加和
funcumsum
(n:int, res:long)
:long =
if(n ==
1) n+res else
cumsum
(n -
1, n+res)
//正常形式函式呼叫尾遞迴函式
fungetcumsum
(n:int)
=cumsum
(n,0
)val res =
getcumsum
(n)
3.1.1 柯里化
柯里化(currying),就是把接受多個引數的函式變換成接受乙個單一引數(最初函式的第乙個引數)的函式,並且返回接受餘下的引數且返回結果的新函式
fun
cumsum
(n:int, res:long)
:long =
if(n ==
1) n+res else
cumsum
(n -
1, n+res)
funcurrying
(func:
(int, long)
->long, res:long)
=fun
(n:int)
=func
(n, res)
val curriedcumsum =
currying
(::cumsum,0)
val res =
curriedcumsum
(n)
通過使用引數預設值,我們可以很輕鬆地將傳入兩個引數轉為傳入乙個引數
fun
cumsum
(n:int, res:long =0)
:long =
if(n ==
1) n+res else
cumsum
(n -
1, n+res)
val res =
cumsum
(n)
在kotlin中,對尾遞迴函式特別進行了優化,要使用優化,除了要將遞迴函式形式改寫為尾遞迴函式外,還要在函式前加上關鍵字tailrec
tailrec
funcumsum
(n:int, res:long =0)
:long =
if(n ==
1) n+res else
cumsum
(n -
1, n+res)
注意:如果沒有新增關鍵字tailrec,即使將遞迴函式形式改寫為尾遞迴函式,kotlin也不會對函式進行尾遞迴優化 Kotlin尾遞迴優化
一 尾遞迴優化 1.遞迴的一種特殊形式 2.呼叫自身後無其他的操作 3.tailrec關鍵字提示編譯器尾遞迴優化 二 具體的來看看一下 說明 package net.println.kotlin.chapter5.tailrecursive author wangdong description 定...
Kotlin 尾遞迴優化
尾遞迴就是函式在呼叫完自己之後沒有其他操作的遞迴,是遞迴的一種特殊形式。舉個例子,計算斐波那契數列第 n 項 的遞迴演算法有哪些?斐波那契數列第 0 1 位都是 1,從第二位開始,每項是前兩位之和,因此用遞迴演算法很容易就能實現出來了 fun fib1 n int int 這種寫法雖然遞迴呼叫是在方...
Kotlin筆記19 尾遞迴優化
前面了我學習了kotlin的遞迴,那麼我還接觸到了kotlin的尾遞迴優化。什麼是尾遞迴優化呢?帶著疑問更好去學習。1.尾遞迴是遞迴的一種特殊形式 2.呼叫自身無其他操作 3.tailrec關鍵字提示編譯器尾遞迴優化 demo中有使用tailrec關鍵字進行提示編譯器尾遞迴優化。尾遞迴演示 實現方式...