尾呼叫尾遞迴及其優化(筆記)

2021-07-16 14:30:30 字數 2953 閱讀 4371

尾呼叫

尾呼叫(tail call)是函式式程式設計的乙個重要概念,本身非常簡單,即指某個函式的最後一步呼叫另乙個函式。

function

f(x)

上述**中,函式的最後一步是呼叫函式g,這就叫尾呼叫。

以下三種情況,都不屬於尾呼叫。

function

f(x)

function

f(x)

function

f(x)

上面**中,第乙個呼叫函式g後還有賦值操作。第二種也是有後續的操作。而第三個則等同於

function

f(x)

尾呼叫之所以與其他呼叫不同,就在於它的特殊的呼叫位置。

我們知道,函式的呼叫會在記憶體姓曾乙個呼叫記錄,又稱為call frame,儲存呼叫位置和內部變數等資訊。如果在函式a的內部呼叫函式b,那麼在a的call frame的上方還會形成乙個b的call frame。等b執行結束,將結果返回a,b的call frame才會消失。如果函式b內部還呼叫c,那麼還有乙個c的call frame,以此類推,所有的call frame 就形成了乙個call stack。

尾呼叫優化

尾呼叫由於是函式的最後一步操作,如果呼叫位置,內部變數等資訊都不會再用到了。尾呼叫時可以刪除此call frame下方的call frame。這就叫做尾呼叫優化(tail call optimization)。

當然,有些尾呼叫還會用到內部變數如:

function

addone

(a) return inner(a);

}

這時候就無法進行尾呼叫優化。

尾遞迴

函式呼叫自身,稱為遞迴。如果尾呼叫自身,就稱為尾遞迴。

遞迴非常的耗費記憶體,因為需要同時儲存成百上千個call frame。很容易發生棧溢位錯誤。對於尾呼叫優化後的尾遞迴來說,因為只有乙個call frame。所以不會存在棧溢位的問題。

如下面階乘計算遞迴:

function

factorail

(n)

這種情況下降無法進行尾呼叫優化。所以這裡的空間複雜度為o(n)。

而如果改寫成尾遞迴並進行優化,則空間複雜度為o(1),像下面這樣

function

factorial

(n, total)

尾遞迴改寫

尾遞迴的實現,往往需要改寫遞迴函式,確保最後一步只呼叫自身。做到這一點的方法,就是把所有用刀的內部變數改寫成函式的引數。像上面的階乘函式改寫一樣。這樣做的缺點是函式不再直觀,或者說會更改函式的呼叫形式。

有兩種方法可以解決這個問題:

一是進行進一步的封裝,並提供預設值

function

tailfactorial

(n, total)

function

factorial

(n)

二是使用(柯理化)currying。意思是將多引數的函式轉換成單引數的函式的形式。

function

curring

(fn, n)

}function

tailfactorail

(n, total)

const factorial = curring(tailfactorail, 1);

factorial(5);

這種方法的原理與第一種類似。只是使用了較為規範的封裝。

尾呼叫優化

前面一直提到尾呼叫優化,那麼尾呼叫優化是怎麼實現的呢。優化的目標就是減少呼叫棧,普通尾呼叫優化實現不清楚。下面闡述尾遞迴函式的優化,尾遞迴優化的策略就是用迴圈替換掉遞迴:

如下面這個遞迴函式:

function

sum(x, y) else

}sum(1, 100000)

上述函式將會產生100000個call frame,通常情況下回報棧溢位異常或錯誤。

蹦床函式

可以使用蹦床函式(trampoline)將遞迴轉化為迴圈

function

trampoline

(f) return f;

}

上面就是蹦床函式的實現,可見其中的引數f必須在執行之後返回乙個同形式的函式。

所以我們需要將遞迴函式改寫成這樣:

function

sum(x, y) else

}

即使用bind將乙個函式形式繫結到sum變數上並返回。以下是bind的作用

bind()方法會建立乙個新函式,當這個新函式被呼叫時,它的this值是傳遞給bind()的第乙個引數, 它的引數是bind()的其他引數和其原本的引數.

上述**中,sum函式每次執行,都會返回自身的另乙個版本(新函式)。像下面這樣呼叫,就不會發生棧溢位

trampoline(sum(1, 100000))
真正的優化策略

然而,蹦床函式並不是真正的尾遞迴的優化, 下面的實現才是。

function

tco(f)

active = false;

return value;}};

}var sum = tco(function

(x, y)

else

});sum(1, 100000)

尾呼叫 尾遞迴

首先什麼是尾呼叫呢?我的理解是在,函式的最後呼叫乙個函式,並不包含該函式的任何變數。如 def f n return g n 複製 這個就是尾呼叫,尾呼叫的乙個好處就是,不用生成呼叫棧,因為假設是個尾呼叫,那麼當我執行到函式末尾的時候,這個函式相關的資訊我都可以不用保留了,因此不會出現棧溢位的問題。...

尾呼叫和尾遞迴

造成這樣的結果是因為每個函式在呼叫另乙個函式的時候,沒有return該呼叫,所以執行引擎會認為你還沒有呼叫完畢,會保留呼叫幀。而如果使用尾呼叫優化,呼叫幀就永遠只有一條,這個時候就會節省很大一部分的記憶體空間,維護了 執行的流暢性。以上 就叫做尾呼叫優化,這個時候呼叫幀就永遠只有一條,節省了部分記憶...

遞迴尾呼叫

什麼是遞迴尾呼叫 遞迴呼叫放在函式結尾 區別 def fac n if n 0 return 1 else return n fac n 1 def print fa n if n 0 print fa n 1 print n print fa 10 結果 1到10def fac n if n 0 ...