如何优化尾调用

开发
经常看到关于尾递归这三个词,递归很多时候,都离不开我们,废话不多说,这次我们梳理一遍关于递归那些事。

 

            [[344640]]

 前言

 

在这里关于递归,这里就不赘述了,有兴趣的可以去查一查资料。

 

需要了解如何优化尾递归的话,我们需要从最开始讲起。

  • 什么是尾调用
  • 什么是尾递归
  • 如何优化尾递归

尾调用
从字面理解,自然而言就是在函数的尾部返回一个函数的调用,通常来说,指的是函数执行的最后一步。

举个例子👇

  1. const fn = () => f1() || f2() 
  2. // 这里的话, f2函数有可能是尾调用,f1不可能是尾调用 

为什么f1函数不是呢,我们看这个函数的等价形式👇

  1. const fn = function () { 
  2.     const flag = f1() 
  3.     if(flag) { 
  4.         return flag 
  5.     } else { 
  6.         return f2() 
  7.     } 

似乎写到这里,根据尾调用定义,我们就明白了,只有f2函数是在尾部调用。

说到这里,为什么要说尾调用呢?我们事先想一想传统的递归,典型的就是首先执行递归调用,然后根据这个递归的返回值并结算结果,那么传统的递归缺点有哪些呢👇

  • 效率低,占内存。
  • 如果递归链过长,可能会stack overflow

那么我们是不是可以做优化呢,这就可以涉及上面提到的尾调用,它的原理是啥呢👇

“按照阮一峰老师在es6的函数扩展中的解释就是:函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
“这里的“调用帧”和“调用栈”,说的应该就是“执行环境”和“调用栈”。因为尾调用时函数的最后一部操作,所以不再需要保留外层的调用帧,而是直接取代外层的调用帧,所以可以起到一个优化的作用。
从上述的描述中,我们视乎可以理解成

  • 它的原理类似于当编译器检测到一个函数调用是尾递归时,它会覆盖当前的活动记录而不是在函数栈中创建一个新的调用记录。
  • 这样子,我们也可以理解成,不同的语言编译器或者是解释器做了尾递归优化,才让它不会爆栈。

既然是这样子的话,尾递归的优化,取决于浏览器,那具体有哪些主流浏览器支持呢👇

safari 和火狐,有兴趣的可以去了解一下,可以写个斐波那契数列数列验证一下。

手动优化
既然我们知道了,很多浏览器对于尾递归的优化支持的浏览器并不多,那你会好奇,当我们使用尾递归进行优化的时候,依然出现栈溢出的错误,那么我们如何解决呢?👇

我在网上看到一个不错的方案,采用的是蹦床函数👇

  1. function trampoline(f) { 
  2.   while (f && f instanceof Function) { 
  3.     f = f(); 
  4.   } 
  5.   return f; 

那么如何使用呢👇

我们拿最常见的斐波那契数列来说吧

  1. function fibonacci(n) { 
  2.   if (n === 0) return 0 
  3.   if (n === 1) return 1 
  4.   return fibonacci(n - 1) + fibonacci(n - 2) 

根据上面的式子,我们可以将其写成迭代形式,用一个变量去缓存它的值👇

  1. function fibonacci (n, ac1 = 0, ac2 = 1) { 
  2.     return n <= 1 ? ac2 :fibonacci(n - 1, ac2, ac1 + ac2); 

其实试过的小伙伴,会发现,当你需要求的n足够大的时候,还是会报错,类似于下面的错误信息👇

  1. // fibonacci(10000) 
  2. Uncaught RangeError: Maximum call stack size exceeded 

这个时候,那么我们如何去优化呢?难道真的没有办法可以解决了吗👇

这里得借鉴下别人的思路,我觉得挺不错的,这里就给出代码👇

  1. function trampoline(f) { 
  2.   while (f && f instanceof Function) { 
  3.     f = f(); 
  4.   } 
  5.   return f; 

你可以把这个函数称之为蹦床函数, 这个函数的作用就是放回一个新的函数,我们将它们俩结合起来的话,栈溢出的问题似乎就可以解决了👇

  1. // 可以试一试噢 
  2. trampoline(fibonacci (10000)) 

这里的蹦床函数,我是参考别人的写法,似乎这样子写的话,不太行,我个人觉得这样子可以避免调用栈溢出,实际情况下,这样子是行不通的,哪里有行不通的,还望指出。

当然了,手动优化,可以将递归的过程改写成迭代的过程,就拿斐波那契数列这题来说,我们可以使用动态规划来完成👇,O(n)完成答案的更新。

  1. // 伪代码 
  2. F[i] = F[i-1] + F[i-2] 

嗯,将一个尾递归函数转换成循环迭代函数,算是手动优化一种方式,在我们语言没有原生支持尾递归优化,那么可以考虑这种情况。

对于尾递归而言,我们需要了解优化它的原理,如果有必要的话,将递归的形式写成迭代的形式,通过迭代方式,降低重复值的计算,当然了,这个过程,有时候是比较难的,值得我们去思考。

 

责任编辑:姜华 来源: 前端UpUp
相关推荐

2019-03-26 08:15:45

iOS尾调用Objective-C

2020-05-27 07:38:36

尾递归优化递归函数

2010-09-17 13:01:44

Python

2009-07-22 07:44:00

Scala尾递归

2011-06-07 15:42:25

优化URL

2012-03-16 16:33:35

视频会议马赛克深信服

2011-06-24 16:44:43

网站优化

2020-04-16 09:44:53

JupyterPython机器学习

2010-01-11 16:31:54

C++优化器

2020-10-16 09:00:12

前端开发技术

2023-10-18 10:38:53

API

2020-10-16 10:40:39

前端性能可视化

2009-11-16 13:59:22

Oracle优化

2023-03-29 07:36:32

链表头插尾插

2013-09-02 16:04:20

Windows

2024-02-19 08:11:40

C++编程尾返回类型推导

2017-05-18 16:40:18

跨公网架构线程

2010-07-28 15:29:18

Flex函数

2010-02-05 15:00:44

Android 调用u

2024-03-12 09:47:10

Redis数据库
点赞
收藏

51CTO技术栈公众号