js遞迴的優化
阿新 • • 發佈:2021-02-03
尾遞迴
函式呼叫自身,稱為遞迴。如果尾呼叫自身,就稱為尾遞迴。
阮一峰在《ECMAScript 6》中舉了一個例子:
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
這是一個常規的Fibonacci 數列遞迴實現。但執行時需要儲存眾多呼叫幀,佔用大量記憶體,容易發生棧溢位錯誤。
但若將其更改為尾遞迴:
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
則只存在一個呼叫幀,空間複雜度為O(1),因此永遠不會發生棧溢位錯誤。
從例子中也可以看出尾遞迴的特徵:最後return
的只有遞迴函式,沒有其他額外運算。
然而,ES6尾遞迴有一個極大的限制:只在嚴格模式下有效,正常模式無效。
常規模式下的優化
在常規模式下,依然是可以手動實現尾遞迴優化的。其思路為:使用迴圈來替換遞迴。
阮一峰舉了一個例子:
var sum = function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1);
} else {
return x;
}
}
sum(1, 100000)
這是一個正常遞迴,也寫成了尾遞迴的形式,便於進行手動優化。
現在實現一個通用的優化函式tco()
,令其套用在尾遞迴函式sum()
上後,可以實現優化。
function tco(f) {
var value;
var active = false;
var accumulated = [];
return function accumulator() {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}
var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});
sum(1, 100000)
針對於該程式碼,阮一峰給出的解釋為:
tco函式是尾遞迴優化的實現,它的奧妙就在於狀態變數active。預設情況下,這個變數是不啟用的。一旦進入尾遞迴優化的過程,這個變數就激活了。然後,每一輪遞迴sum返回的都是undefined,所以就避免了遞迴執行;而accumulated陣列存放每一輪sum執行的引數,總是有值的,這就保證了accumulator函式內部的while迴圈總是會執行。這樣就很巧妙地將“遞迴”改成了“迴圈”,而後一輪的引數會取代前一輪的引數,保證了呼叫棧只有一層。
若不理解優化的思路,那麼該解釋難以看懂。
該程式碼的流程圖為:
其中:
- 呼叫
sum()
,sum()
對tco()
返回的函式傳入一個自定義的function()
。tco()
返回的是accumulator()
。對於accumulator()
而言,有3個公共的變數:value
、active
、accumulated
。 - 將引數
arguments
壓入到accumulated
中。此時accumulated
為[[1, 10000]]
。 - 第一次
active
是false
,故而可以進入if
。然後將active
置為true
。這樣做是為了阻止接下來進入遞迴。 - 進入
while
,accumulated.shift()
的作用是刪除0號元素並返回該元素。故而此時拿到了剛剛儲存的引數[1, 10000]
,並清空了accumulated
。 - 執行
sum()
內的函式。於是執行到了sum(x + 1, y - 1)
,引數變為[2, 9999]
,然後第二次進入了sum()
,開始遞迴。 - 於是再次進入
accumulator()
,此時公共變數accumulated
大小為0。將[2, 9999]
塞給它,大小變為1。 - 判斷
if
。由於在第3步中active
置為true
,故而此時if
判斷為false
,無法進入。因此遞迴被中斷。 - 但
if
後沒有任何邏輯了,因此執行完後返回的是undefined
。所以此時返回到第5步value = f.apply(this, accumulated.shift());
,因此該value
的值是undefined
。 - 再次進入
while
判斷。由於第6步將遞迴的引數塞給了accumulated
,因此while
判斷為true
,繼續執行。此時再次從第4步開始,構成迴圈。 - 不斷重複4-9步,直到
sum()
內的if
判定為false
,即y == 0
。此時就會返回x
的最終運算結果。
綜上,可以看出該優化截斷了遞迴,並通過儲存遞迴引數的方式,將遞迴運算轉換為迴圈,從而避免了棧溢位。