尾調用和尾遞歸

尾調用

尾調用(Tail Call)是函數式編程的一個重要概念,自己很是簡單,一句話就能說清楚,就是指某個函數的最後一步是調用另外一個函數。編程

//尾調用
function f(x) {return g(x);}
//上面代碼中,函數f的最後一步是調用函數g,這就叫尾調用

//不屬於尾調用舉例
// 狀況一
function f(x){
  let y = g(x);
  return y;
}
// 狀況二
function f(x){
  return g(x) + 1;
}
// 狀況三
function f(x){
  g(x);
}

尾調用優化

尾調用之因此與其餘調用不一樣,就在於它的特殊的調用位置。函數式編程

咱們知道,函數調用會在內存造成一個「調用記錄」,又稱「調用幀」(call frame),保存調用位置和內部變量等信息。若是在函數A的內部調用函數B,那麼在A的調用幀上方,還會造成一個B的調用幀。等到B運行結束,將結果返回到A,B的調用幀纔會消失。若是函數B內部還調用函數C,那就還有一個C的調用幀,以此類推。全部的調用幀,就造成一個「調用棧」(call stack)。函數

尾調用因爲是函數的最後一步操做,因此不須要保留外層函數的調用幀,由於調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用幀,取代外層函數的調用幀就能夠了。優化

function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();

// 等同於
function f() {
  return g(3);
}
f();

// 等同於
g(3);

上面代碼中,若是函數g不是尾調用,函數f就須要保存內部變量m和n的值、g的調用位置等信息。但因爲調用g以後,函數f就結束了,因此執行到最後一步,徹底能夠刪除 f(x) 的調用幀,只保留 g(3) 的調用幀。this

這就叫作「尾調用優化」(Tail call optimization),即只保留內層函數的調用幀。若是全部函數都是尾調用,那麼徹底能夠作到每次執行時,調用幀只有一項,這將大大節省內存。這就是「尾調用優化」的意義。spa

注意,只有再也不用到外層函數的內部變量,內層函數的調用幀纔會取代外層函數的調用幀,不然就沒法進行「尾調用優化」code

尾遞歸

函數調用自身,稱爲遞歸。若是尾調用自身,就稱爲尾遞歸。blog

遞歸很是耗費內存,由於須要同時保存成千上百個調用幀,很容易發生「棧溢出」錯誤(stack overflow)。但對於尾遞歸來講,因爲只存在一個調用幀,因此永遠不會發生「棧溢出」錯誤。  以斐波那契數列(Fibonacci)爲例分析:遞歸

//普通遞歸
function Fibonacci (n) {
  if ( n <= 1 ) {return 1};

  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Fibonacci(10); // 89
// Fibonacci(100)
// Fibonacci(500)
// 堆棧溢出了

//尾遞歸
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};

  return Fibonacci2 (n - 1, ac2, ac1 + ac2);  //保留一個調用記錄,複雜度O(1)
}

Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity

遞歸函數的優化

尾遞歸的實現,每每須要改寫遞歸函數,確保最後一步只調用自身。作到這一點的方法,就是把全部用到的內部變量改寫成函數的參數。這樣作的缺點是不太直觀,解決辦法有兩種:內存

方法1:在尾遞歸函數以外,再提供一個正常形式的函數。

//尾遞歸
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};

  return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity

//優化1
function tailFibonacci(n, ac1, ac2){
  if( n <= 1 ) {return ac2};

  return tailFibonacci (n - 1, ac2, ac1 + ac2);    
}
function Fibonacci3(n){
    return tailFibonacci(n, 1 , 1);
}
Fibonacci3(100) // 573147844013817200000

方法2:函數式編程有一個概念,叫作柯里化(currying),意思是將多參數的函數轉換成單參數的形式。這裏也可使用柯里化。

//優化2 柯里化
function currying(fn, n1, n2) {
  return function (m) {
    return fn.call(this, m, n1, n2);
  };
}
function tailFibonacci(n, ac1, ac2){
  if( n <= 1 ) {return ac2};

  return tailFibonacci (n - 1, ac2, ac1 + ac2);    
}
const Fibonacci4 = currying(tailFibonacci,1,1);

Fibonacci4(100)  // 573147844013817200000
相關文章
相關標籤/搜索