尾调用优化
一、什么是尾调用?
当函数的最后一步是直接返回调用另一个函数,那么这就叫尾调用。
function a(x){
return b(x-1);
}
上面代码,函数a最后一步是调用函数b,这就叫尾调用。
上面代码,函数a最后一步是调用函数b,这就叫尾调用。
function a(x){
if (x > 0) return b(x)
return c(x);
}
上面代码,函数b和c都属于尾调用,因为它们都是函数a的最后一步操作。 以下两种情况,都不属于尾调用。
function a(x){
let y = b(x);
return y;
}
调用函数b之后,用变量保存,再返回变量值.即使结果一致,但语义不同.
function a(x){
return b(x) + 1;
}
调用后还有操作,也是不行的。
二、尾调用优化
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。
如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。
如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。
而尾调用时,由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。
function a() {
let m = 1;
let n = 2;
return f(m + n);
}
//等同于
function a() {
return f(3);
}
//等同于
f(3);
上面代码中,如果函数f()不是尾调用,函数a()就需要保存内部变量m和n的值、f()的调用位置等信息。
但由于调用f()之后,函数a()就结束了,所以执行到最后一步,完全可以删除a() 的调用记录,只保留f(3)的调用记录。
但由于调用f()之后,函数a()就结束了,所以执行到最后一步,完全可以删除a() 的调用记录,只保留f(3)的调用记录。
这就叫做"尾调用优化"(Tail call optimization),即只保留内层函数的调用记录。
如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。
如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。
三、尾递归
如果递归函数尾调用自身,就称为尾递归。
递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。
对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。
对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。
//计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n)
function factorial(n){
if (n === 1) return 1;
return n * factorial(n - 1);
}
//改写成尾递归,只保留一个调用记录,复杂度 O(1)
function factorial(n, total = 1){
//total是用来保存累计的结果值
if(n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
四、递归函数的改写
尾递归的实现,需要确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。
递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,所以尾递归对这些语言极其重要。
对于其他支持"尾调用优化"的语言(比如Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。
来源: http://www.ruanyifeng.com/blog/2015/04/tail-call.html
递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,所以尾递归对这些语言极其重要。
对于其他支持"尾调用优化"的语言(比如Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。
来源: http://www.ruanyifeng.com/blog/2015/04/tail-call.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术