美文网首页
递归调用优化

递归调用优化

作者: 99ZY | 来源:发表于2021-11-29 21:26 被阅读0次

尾递归优化

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}
factorial(5) // 120

上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。

如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}
factorial(5, 1) // 120

还有一个比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性。

非尾递归的 Fibonacci 数列实现如下。

function Fibonacci (n) {
  if ( n <= 1 ) {return 1};
  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Fibonacci(10) // 89
Fibonacci(100) // 超时
Fibonacci(500) // 超时

缓存策略优化

var  cache = {"1":1};
function Fibonacci (n) {
 if(cache[n]){
   return cache[n];
 }
 if ( n <= 1 ) {return 1};
 v = Fibonacci(n - 1) + Fibonacci(n - 2);
 return  cache[n] = v;
 // 缓存策略会大大降低计算次数!
}

尾递归优化过的 Fibonacci 数列实现如下。

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
    //   三叉树
  function Fibonacci_3U(n, p1 = 1, p2 = 1, p3 = 1) {
           if (n <= 1) {
                    return p3
            }
            return Fibonacci_3U(n - 1, p2, p3, p1 + p2 + p3)
    }
    console.log("Fibonacci_3:", Fibonacci_3U(5)) // 17

由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 亦是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存。

注意点

ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

func.arguments:返回调用时函数的参数。
func.caller:返回调用当前函数的那个函数。
尾调用优化发生时,函数的调用栈会改写(动态指向正在执行的函数),因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。

function restricted() {
  'use strict';
  restricted.caller;    // 报错
  restricted.arguments; // 报错
}
restricted();

尾递归优化的实现

尾递归优化只在严格模式下生效,那么正常模式下,或者那些不支持该功能的环境中,有没有办法也使用尾递归优化呢?回答是可以的,就是自己实现尾递归优化。

它的原理非常简单。尾递归之所以需要优化,原因是调用栈太多,造成溢出,那么只要减少调用栈,就不会溢出。怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”。

下面是一个正常的递归函数。

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);
    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) {
    //   递归遇到active位active === true时候,中断递归,while继续执行
    return sum(x + 1, y - 1)
  }
  else {
    return x
  }
});

sum(1, 100000)  // 100001

上面代码中,tco函数是尾递归优化的实现,它的奥妙就在于状态变量active。默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。f.apply调用,会导致递归sum,都会被active拒之门外,但是accumulated把本次的参数push存储了起来,然后结束f.apply调用,又判断while,此时又有了值,继续执行while内语句,所以就避免了递归执行;这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。

      //   三叉树
      var Fibonacci_3U_U = tco(function (num, p1 = 1, p2 = 1, p3 = 1) {
                if (num <= 1) {
                    return p3
                }
                return Fibonacci_3U_U(num - 1, p2, p3, p1 + p2 + p3)
            })
            console.log("Fibonacci_3U_U:", Fibonacci_3U_U(5))

相关文章

  • 什么是尾调用?什么是尾递归?尾调用的优化?尾递归优化?

    尾调用优化 尾递归(尾调用优化)

  • 尾递归优化

    “尾递归优化”的含义是:如果递归函数属于尾递归,那么运行时会优化其调用过程。优化主要针对调用栈,将多层调用,转化为...

  • Kotlin语言(九):特性

    1、尾递归优化 尾递归:函数在调用自己之后没有再执行其他任何操作就是尾递归 尾递归优化的原理就是将递归转换成迭代,...

  • 递归调用优化

    尾递归优化 函数调用自身,称为递归。如果尾调用自身,就称为尾递归。 递归非常耗费内存,因为需要同时保存成千上百个调...

  • 9. 递归函数

    使用递归函数需要注意防止栈溢出解决递归调用栈溢出的方法是通过尾递归优化遗憾的是,大多数编程语言没有针对尾递归做优化...

  • JavaScript 函数式编程 - 蹦床,递归优化

    Trampoline 解决 Blowing the stack JavaScript 引擎没有对递归调用优化。因此...

  • 7.尾递归优化

    尾递归:最后一行调用自身之后没有任何操作直接返回kotlin尾递归优化,关键字tailrec如: 不优化的话大量的...

  • 尾递归

    尾递归:递归调用返回的结果总是被直接返回,有助于编译器优化为普通循环 具有以下特点: 1. 调用自身函数 2. 仅...

  • iOS objc_msgSend尾调用优化机制详解

    级别:★★☆☆☆标签:「objc_msgSend」「尾调用优化」「尾递归」作者: WYW、MrLiuQ审校: Qi...

  • 尾调用优化

    尾调用优化 尾递归 正常递归 尾递归 改写以上代码,使其只有一个参数: 总结一下,递归本质上是一种循环操作。纯粹的...

网友评论

      本文标题:递归调用优化

      本文链接:https://www.haomeiwen.com/subject/rblhxrtx.html