尾调用优化和尾递归改写

1 尾调用

尾调用就是指某个函数的最后一步是调用另一个函数。

# 是尾调用
def f(x):
    return g(x)

# 不是尾调用,因为调用函数后还要执行加法,加法才是最后一步操作
def f(x):
    return 1+g(x)

2 尾调用优化

函数调用有一个调用栈,栈内保存了这个函数内部的变量信息。函数掉用就是切换不同的调用帧,从而保证每个函数有独立的运行环境。因为尾调用是函数的最后一步操作,所以在进入被尾调用函数之前并不需要保留外层函数的运行时环境,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。这就叫做"尾调用优化"(Tail call optimization),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。

尾递归

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

def factorial(n) {
  if (n == 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

def factorial(n, total) {
  if (n == 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

4 递归函数的改写

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total ,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算5的阶乘,需要传入两个参数5和1?

两个方法可以解决这个问题。方法一是在尾递归函数之外,再提供一个正常形式的函数。

def tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

def factorial(n) {
  return tailFactorial(n, 1);
}

factorial(5) // 120

上面代码通过一个正常形式的阶乘函数 factorial ,调用尾递归函数 tailFactorial ,看起来就正常多了。

原文地址:https://www.cnblogs.com/ZeroTensor/p/10503607.html

时间: 2024-10-10 20:16:11

尾调用优化和尾递归改写的相关文章

尾调用优化

参考:http://www.ruanyifeng.com/blog/2015/04/tail-call.html 感谢阮老师. 尾调用的概念非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数. //正确的尾调用 function f(x) { if (x > 0) { return m(x) } return n(x); } function f(x){ return g(x); } //非尾调用 // 情况一 function f(x){ let y = g(x); retu

详解JavaScript调用栈、尾递归和手动优化

调用栈(Call Stack) 调用栈(Call Stack)是一个基本的计算机概念,这里引入一个概念:栈帧. 栈帧是指为一个函数调用单独分配的那部分栈空间. 当运行的程序从当前函数调用另外一个函数时,就会为下一个函数建立一个新的栈帧,并且进入这个栈帧,这个栈帧称为当前帧.而原来的函数也有一个对应的栈帧,被称为调用帧.每一个栈帧里面都会存入当前函数的局部变量. 当函数被调用时,就会被加入到调用栈顶部,执行结束之后,就会从调用栈顶部移除该函数.并将程序运行权利(帧指针)交给此时栈顶的栈帧.这种后进

尾调用及递归优化

ES6 规范中添加了对尾调用优化的支持,虽然目前来看支持情况还不是很好,但了解其原理还是很有必要的,有助于我们编写高效的代码,一旦哪天引擎支持该优化了,将会收获性能上的提升. 讨论尾调用前,先看函数正常调用时其形成的堆栈(stack frame)情况. 函数的调用及调用堆栈 先看一个概念:调用堆栈(call stack),或叫调用帧(stack frame). 我理解的调用堆栈:因为函数调用后,流程会进入到被调用函数体,此时需要传入一些入参,这些入参需要存放到一个位置,另外当函数调用结束后,执行

栈溢出 缓冲区溢出的一种 缓冲区以外的存储单元被改写 优化方法之一:尾调用

个人:尾调用时函数式编程的一个重要概念, 栈溢出: 函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息.如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧.等到B运行结束,将结果返回到A,B的调用帧才会消失.如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推.所有的调用帧,就形成一个"调用栈"(call stack).递归调用非常耗内存. 尾调用: 就是指某

函数式编程-尾递归、尾调用

一.什么是尾调用? 尾调用的概念非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数. function f(x){ return g(x); } 上面代码中,函数f的最后一步是调用函数g,这就叫尾调用. 以下两种情况,都不属于尾调用. // 情况一 function f(x){ let y = g(x); return y; } // 情况二 function f(x){ return g(x) + 1; } 上面代码中,情况一是调用函数g之后,还有别的操作,所以不属于尾调用,

尾调用

转自: http://www.ruanyifeng.com/blog/2015/04/tail-call.html https://stackoverflow.com/questions/310974/what-is-tail-call-optimization 一.什么是尾调用? 尾调用的概念非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数. function f(x) {returng(x);} 上面代码中,函数f的最后一步是调用函数g,这就叫尾调用. 以下两种情况,都不

学习Javascript之尾调用

前言 本文2433字,阅读大约需要10分钟. 总括: 本文介绍了尾调用,尾递归的概念,结合实例解释了什么是尾调用优化,并阐述了尾调用优化如今的现状. 参考文章:尾递归的后续探究 公众号:「前端进阶学习」,回复「666」,获取一揽子前端技术书籍 事亲以敬,美过三牲. 正文 尾调用是函数式编程的一个重要的概念,本篇文章就来学习下尾调用相关的知识. 尾调用 在之前的文章理解Javascript的高阶函数中,有说过在一个函数中输出一个函数,则这个函数可以被成为高阶函数.本文的主角尾调用和它类似,如果一个

[Java 8] Lambda表达式对递归的优化 - 使用尾递归

递归优化 很多算法都依赖于递归,典型的比如分治法(Divide-and-Conquer).但是普通的递归算法在处理规模较大的问题时,常常会出现StackOverflowError.处理这个问题,我们可以使用一种叫做尾调用(Tail-Call Optimization)的技术来对递归进行优化.同时,还可以通过暂存子问题的结果来避免对子问题的重复求解,这个优化方法叫做备忘录(Memoization). 本文首先对尾递归进行介绍,下一票文章中会对备忘录模式进行介绍. 使用尾调用优化 当递归算法应用于大

lua报错,看到报错信息有tail call,以为和尾调用有关,于是查了一下相关知识

尾调用是指在函数return时直接将被调函数的返回值作为调用函数的返回值返回,尾调用在很多语言中都可以被编译器优化, 基本都是直接复用旧的执行栈, 不用再创建新的栈帧, 原理上其实也很简单, 因为尾调用在本质上看的话,是整个子过程调用的最后执行语句, 所以之前的栈帧的内容已经不再需要, 完全可以被复用.报错的回溯日记,因为旧的执行栈已经没了,所以报错日记只显示(tail call).一般调用栈的长度为1M到2M,保存了调用过程中的参数和相关环境,如果递归调用太长,就会溢出.尾调用就能解决递归函数