1. 程式人生 > 其它 >js遞迴的優化

js遞迴的優化

技術標籤:Javascriptjs遞迴優化阮一峰tco

尾遞迴

函式呼叫自身,稱為遞迴。如果尾呼叫自身,就稱為尾遞迴。
阮一峰在《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迴圈總是會執行。這樣就很巧妙地將“遞迴”改成了“迴圈”,而後一輪的引數會取代前一輪的引數,保證了呼叫棧只有一層。

若不理解優化的思路,那麼該解釋難以看懂。

該程式碼的流程圖為:
process

其中:

  1. 呼叫sum()sum()tco()返回的函式傳入一個自定義的function()tco()返回的是accumulator()。對於accumulator()而言,有3個公共的變數:valueactiveaccumulated
  2. 將引數arguments壓入到accumulated中。此時accumulated[[1, 10000]]
  3. 第一次activefalse,故而可以進入if。然後將active置為true。這樣做是為了阻止接下來進入遞迴。
  4. 進入whileaccumulated.shift()的作用是刪除0號元素並返回該元素。故而此時拿到了剛剛儲存的引數[1, 10000],並清空了accumulated
  5. 執行sum()內的函式。於是執行到了sum(x + 1, y - 1),引數變為[2, 9999],然後第二次進入了sum(),開始遞迴。
  6. 於是再次進入accumulator(),此時公共變數accumulated大小為0。將[2, 9999]塞給它,大小變為1。
  7. 判斷if。由於在第3步中active置為true,故而此時if判斷為false,無法進入。因此遞迴被中斷。
  8. if後沒有任何邏輯了,因此執行完後返回的是undefined。所以此時返回到第5步value = f.apply(this, accumulated.shift());,因此該value的值是undefined
  9. 再次進入while判斷。由於第6步將遞迴的引數塞給了accumulated,因此while判斷為true,繼續執行。此時再次從第4步開始,構成迴圈。
  10. 不斷重複4-9步,直到sum()內的if判定為false,即y == 0。此時就會返回x的最終運算結果。

綜上,可以看出該優化截斷了遞迴,並通過儲存遞迴引數的方式,將遞迴運算轉換為迴圈,從而避免了棧溢位。