遞歸函數及遞歸優化(尾遞歸)


一、定義

       在函數內部,可以調用其他函數。如果一個函數在內部調用自身本身,這個函數就是遞歸函數。

二、利弊

       遞歸函數的優點是定義簡單,邏輯清晰。理論上,所有的遞歸函數都可以寫成循環的方式,但循環的邏輯不如遞歸清晰。

       使用遞歸函數需要注意防止棧溢出。在計算機中,函數調用是通過棧(stack)這種數據結構實現的,每當進入一個函數調用,棧就會加一層棧幀,每當函數返回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞歸調用的次數過多,會導致棧溢出。

三、優化

       解決遞歸調用棧溢出的方法是通過尾遞歸優化,事實上尾遞歸和循環的效果是一樣的,所以,把循環看成是一種特殊的尾遞歸函數也是可以的。

        尾遞歸是指,在函數返回的時候,調用自身本身,並且,return語句不能包含表達式。這樣,編譯器或者解釋器就可以把尾遞歸做優化,使遞歸本身無論調用多少次,都只占用一個棧幀,不會出現棧溢出的情況。

 普通遞歸

function f(x) {
   if (x === 1) return 1;
   return 1 + f(x-1);
}

尾遞歸的判斷標准是函數運行【最后一步】是否調用自身,而不是是否在函數的【最后一行】調用自身。 
尾遞歸

function f(x) {
   if (x === 1) return 1;
   return f(x-1);
}

 

普通的一個實現階乘的函數,一般會這么寫

function factorial(n){

if( n === 1) return n;

return n * factorial(n-1);

}
  • 這樣會保存n調記錄,復雜程度要吐血

如果可以改成寫尾遞歸呢(只用保留一個調用記錄)

      尾遞歸調用時,如果做了優化,棧不會增長,因此,無論多少次調用也不會導致棧溢出。

function factorial(n,total){

if( n === 1 ) return total;

return factorial(n-1,n*total);

}

factorial(5,1); //輸出120

 

尾遞歸優化

尾遞歸優化只在嚴格模式下生效,那么正常模式下,或者那些不支持該功能的環境中,有沒有辦法也使用尾遞歸優化呢?回答是可以的,就是自己實現尾遞歸優化。

它的原理非常簡單。尾遞歸之所以需要優化,原因是調用棧太多,造成溢出,那么只要減少調用棧,就不會溢出。怎么做可以減少調用棧呢?就是采用“循環”換掉“遞歸”。

 

下面是一個正常的遞歸函數。

function sum(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1);
  } else {
    return x;
  }
}

sum(1, 100000)
// Uncaught RangeError: Maximum call stack size exceeded(…)

上面代碼中,sum是一個遞歸函數,參數x是需要累加的值,參數y控制遞歸次數。一旦指定sum遞歸100000次,就會報錯,提示超出調用棧的最大次數。

蹦床函數(trampoline)可以將遞歸執行轉為循環執行。

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}

上面就是蹦床函數的一個實現,它接受一個函數f作為參數。只要f執行后返回一個函數,就繼續執行。注意,這里是返回一個函數,然后執行該函數,而不是函數里面調用函數,這樣就避免了遞歸執行,從而就消除了調用棧過大的問題。

然后,要做的就是將原來的遞歸函數,改寫為每一步返回另一個函數。

function sum(x, y) {
  if (y > 0) {
    return sum.bind(null, x + 1, y - 1);
  } else {
    return x;
  }
}

上面代碼中,sum函數的每次執行,都會返回自身的另一個版本。

現在,使用蹦床函數執行sum,就不會發生調用棧溢出。

trampoline(sum(1, 100000))
// 100001

蹦床函數並不是真正的尾遞歸優化,下面的實現才是

function tco(f) {
  var value;
  var active = false;
  var accumulated = [];

  return function accumulator() {
    accumulated.push(arguments);//每次將參數傳入. 例如, 1 100000
    if (!active) {
      active = true;
      while (accumulated.length) {//出循環條件, 當最后一次返回一個數字而不是一個函數時, accmulated已經被shift(), 所以出循環
        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)//重點在這里, 每次遞歸返回真正函數其實還是accumulator函數
  }
  else {
    return x
  }
});

sum(1, 100000);//實際上現在sum函數就是accumulator函數
// 100001

上面代碼中,tco函數是尾遞歸優化的實現,它的奧妙就在於狀態變量active。默認情況下,這個變量是不激活的。一旦進入尾遞歸優化的過程,這個變量就激活了。然后,每一輪遞歸sum返回的都是undefined,所以就避免了遞歸執行;而accumulated數組存放每一輪sum執行的參數,總是有值的,這就保證了accumulator函數內部的while循環總是會執行。這樣就很巧妙地將“遞歸”改成了“循環”,而后一輪的參數會取代前一輪的參數,保證了調用棧只有一層。


參考:https://www.jianshu.com/p/077e52d60955
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM