尾调用优化

1、概述

  尾调用是函数式编程的一个重要概念,是指某个函数的最后一步是调用另一个函数。

function f(x){
    return g(x);
}   

以下三种情况,都不属于尾调用:

function f(x){
    let y = g(x);
    return y;
}
-----------------------------
function f(x){
    return g(x) + 1;
} 
-----------------------------
function f(x){
    g(x); 
}  // 相当于 return undefined;

2、尾调用优化

  函数调用会在内存形成一个“调用记录”,又称“调用帧”,保存调用位置和内存变量等信息。所有的“调用帧”会形成一个“调用栈”。
  尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧取代外层函数的调用帧就可以了。如:

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

上面代码中,完全可以删除 f(x) 的调用帧,只保留 g(3) 的调用帧。这叫做“尾调用优化”,即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存,这就是“尾调用优化”的意义。

注意:
  (1)只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则无法进行“尾调用优化”。
  (2)ES6 的尾调用优化只在严格模式下开启,正常模式无效。

3、尾递归

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

function Fibonacci(n) {
    if(n === 1){ return 1;}
    if(n === 0){ return 0;}
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}
function op_Fibonacci(n, ac1 = 0, ac2 = 1){
    if(n <= 0) { return ac1;}
    return op_Fibonacci(n - 1,  ac2, ac1 + ac2)
}
Fibonacci(10) // 55
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出
op_Fibonacci(100) // 354224848179262000000
op_Fibonacci(1000) // 4.346655768693743e+208
op_Fibonacci(10000) // Infinity

4、递归函数的改写

  尾递归是实现,往往需要改写递归函数,确保最后只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。如上面 Fibonacci 数列的实现。这样做的缺点就是不太直观,第一眼很难看出来,为什么要传这么多参数。
  两个方法可以解决这个问题:方法一(柯里化),是在尾递归函数之外,再提供一个正常形式的函数。方法二,采用 ES6 的函数默认值。
  柯里化,意思是将多参数的函数转换成单参数的形式。

5、尾递归优化的实现

  尾递归优化只在严格模式下生效,那么正常模式下,就得自己实现尾递归优化。它的原理非常简单,尾递归之所以需要优化,原因是调用栈太多,造成溢出,那么只要减少调用栈就不会溢出。方法一,改写为循环;方法二,借助蹦床函数。
  蹦床函数的原理是接受一个函数作为参数,在蹦床函数内部执行函数,如果函数的返回值还是一个函数,就继续执行。

function trampoline(f){
    while(f && f instanceof Function){
        f = f();
    }
    return f;
}
function op_Fibonacci(n, ac1 = 0, ac2 = 1){
    if(n <= 0) { return ac1;}
    return op_Fibonacci.bind(null, n - 1,  ac2, ac1 + ac2)
}
trampoline(op_Fibonacci(5));  => 5

  上面两种并不是真正的尾递归优化,因为上面两种方法都改写了尾递归函数本身,而真正的尾递归优化应该是非入侵式的,下面是尾递归优化的一种实现。

function tailCallOptimize(f){
    let value,
        active = false;
    const accumulated = [];
    return function accumulator(){
        accumulated.push(arguments);
        if(!active){
            actice = true;
            while(accumulated.length){
                value = f.apply(this, accumulated.shift());
            }
            active = false;
            return value;
        }
    }
}
const fibonacciTail = tailCallOptimize(function(n, a = 0, b = 1){
    if(n === 0) return a;
    return fibonacciTail(n - 1, b, a+b);
});

fibonacciTail(5) => 5

  上述代码,首先通过闭包,在 tailCallOptimize 的作用域中保存唯一的 active 和 accumulated, 其中 active 指示尾递归优化过程是否开始, accumulated 用来存放每次递归调用的参数, push 方法将参数入列, shift 方法将参数出列,保证先进先出顺序执行。
  其次,经过 tailCallOptimeize 包装后返回的是一个新的函数 accumulator ,执行 fibonacciTail 时实际执行的是这个函数,第一次执行时,现将 arguments0 推入队列,active 会被标记为 true ,然后进入 while 循环,取出 arguments0 。在 while 循环执行中,会将参数类数组 arguments1 推入 accumulated 队列,然后直接返回 undefined ,不会递归调用增加调用栈。
  随后,while 循环会发现 accumulated 中有多了一个 arguments1 ,然后再将 arguments2 推入队列。这样 while 循环中对 accumulated 的操作就是进一个,拿一个,再放一个,再拿一个,以此类推。
  最后一次 while 循环返回的就是尾递归的结果了。

阮一峰:ECMAScript 6入门

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python解释器没有针对尾递归优化,所以即使在Python中使用尾递归方式编写函数,也可能导致栈溢出的问题。 尾递归优化可以在函数返回的时候,调用自身本身,并且,在return语句中不包含表达式,这样编译器或者解释器就可以将尾递归进行优化。最后的结果是,不管递归函数进行多少次调用,都只占用一个栈帧,从而避免了栈溢出的情况。然而,Python解释器并没有对尾递归进行优化,所以即使使用尾递归方式编写函数,也可能导致栈溢出问题的出现。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [python基础编程:Python递归及尾递归优化操作实例分析](https://blog.csdn.net/haoxun11/article/details/104976941)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [python-递归函数及尾递归优化](https://blog.csdn.net/wdnysjqr/article/details/80374203)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [Python尾递归优化实现代码及原理详解](https://download.csdn.net/download/weixin_38545923/13705703)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值