JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

笨鸟先飞
• 阅读 1451

这是Jerry 2021年的第 12 篇文章,也是汪子熙公众号总共第 283 篇原创文章。

今天是2021年1月20日,看看历史上的今天都发生了什么。

2004年1月20日,第一个公开版本的Scala发布。

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)
Scala是一种采用静态类型系统的编译型语言,具有很强的可扩展性(Scalability),这也是其名称的由来。

Scala设计初衷是集成面向对象编程和函数式编程的各种特性,运行于JVM平台上,并兼容已有的Java程序。

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

Jerry没有在SAP标准产品开发中使用过Scala,只是完成2015年公司一个内部培训布置的课程作业中,使用Scala在Spark上开发了一个最简单的demo:统计海量英文图书里,计算出使用频率最高的十大单词。

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)
JavaScript, ABAP和Scala里的尾递归(Tail Recursion)
Spark是一个使用Scala编程语言实现的专为大规模数据处理而设计的快速通用的计算引擎。本文不会讨论Spark,而是从Scala语言里,下图第11行的注解@tailrec谈起:尾递归(Tail Recursion).

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

每个程序员对递归的概念都耳熟能详,那什么是尾递归呢?

顾名思义,如果一个函数中递归形式的调用,出现在函数的末尾,且除了该递归调用外,不包含其他的运算操作,则我们称该递归函数是尾递归函数。

本文用阶乘算法来介绍尾递归的概念。

下图红色区域内是阶乘算法的常规递归实现,蓝色区域是阶乘算法的尾递归实现版本。在常规递归算法的末尾,第8行语句(绿色),除了递归调用factorial函数外,还包含一个同n的乘法操作,所以整个函数factorial不能算作尾递归函数。

而尾递归版本中,第14行函数末尾(黄色),仅仅包含函数本身的递归调用,所以整个函数tailFactorial是一个尾递归函数。

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

尾递归函数存在的意义是什么呢?要回答这个问题,我们可以先在单步调试模式下,观察常规递归函数的执行过程。

我们首先使用常规递归函数,计算5的阶乘。

输入参数n为5,执行到第7行,5的阶乘等于5乘以4的阶乘。单步调试进去,输入参数n = 5, 进入第7行,准备执行 5 * factorial(4) .

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

注意观察下图的Call Stack列表,此时我们已经有两个factorial函数的调用栈帧了。

什么是栈帧?复习一下大学计算机原理学到的知识:在函数执行过程中,每一个函数调用都会把当前函数的调用信息和内部变量保存在栈里面,称为一个栈帧(Stack Frame).

其中下图序号为1的栈帧,保存了n = 5的计算上下文;序号为2的栈帧即当前最顶层的栈帧,保存了n = 4的计算上下文。

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

因为只有当n = 1时递归才会结束,而当前n = 4,所以继续单步调试第7行:又生成了一个n = 3的栈帧:

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

n = 2:

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

终于我们来到了n = 1的上下文。看下图Call Stack里的栈帧列表,最顶层的栈帧代表当前n = 1的计算上下文。此时我们已经知道n = 1的阶乘结果如何计算了,即为1本身。

第5行代码返回1的阶乘计算结果1,这行语句返回之后,当前序号为5的栈帧就会被销毁,即将回到下一层序号为4的栈帧去。

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

此时只剩4个栈帧了,最顶层代表n = 2的栈帧。因为现在1的阶乘结果已经出来了,所以2的阶乘结果也能计算了,为2乘以1.

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

2的阶乘返回后,现在只剩3个栈帧,最顶层为n = 3的计算上下文。3的阶乘也能计算了,为3乘以前一个栈帧返回的计算结果,即2的阶乘结果,所以最后为3 × 2 = 6. 如下图所示:

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

4的阶乘计算,此时只剩两个栈帧:

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

5的计算结果,回到最初最先被压到堆栈底部的n = 5的栈帧。计算完毕,5的阶乘为120.

是不是体现出了《数据结构》教科书上关于栈“先进后出”的工作原理?

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

下面再来看看用尾递归实现的阶乘。

下图第20行语句是以尾递归方式计算5的阶乘入口,调用尾递归函数tailFactorial,注意函数的第二个输入参数total,这个参数用于存储当前阶乘的计算结果。

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

这个尾递归函数的结束条件是,当第一个输入参数n为1时,就把第二个输入参数的值,作为阶乘运算的最终结果返回。第二个参数实际上存放的,是当前递归调用的阶乘计算结果。

当n大于1时,递归尚未满足退出条件,此时首先将n和当前的阶乘计算结果(变量total)相乘,将乘积作为第二个输入参数,传递到下一层递归调用的栈帧中去。

下图是tailFactorial函数内部,即将进入第一轮递归调用的栈帧:

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

第一轮递归调用的栈帧内部,序号为2.

注意,此时序号为1的栈帧已经完全不再需要了,因为我们继续进行递归调用的所需信息,都已经包含在第16行tailFactorial调用的两个输入参数里了,此时n为上一层递归调用传入的5 - 1 = 4,total为上一轮传入的5 × 1 = 5. 进行下一轮递归调用,两个输入参数的值分别是4 - 1 = 3和4 * 5 = 20.

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

进入第三层递归调用,此时输入参数 n = 3,total = 20,均为上一层调用传入。

注意,下图标号为1和2的两个栈帧,实际上不再需要了,因为要继续进行递归调用的所有输入信息,都已经存储在标号为3的栈帧里了:

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

n = 2, total = 60,同理,标号为1,2,3的栈帧都不再需要了。

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

n = 1,total = 120,终于计算结束了!这就是5的阶乘,如何通过尾递归的方式计算出来的全过程。

我们在标号为5的栈帧里得到了最终的结果,而此时虽然栈帧1~4还存在,但实际上已经毫无用处了。

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

因为按照尾递归版本的阶乘实现,每一轮阶乘的递归计算结果,已经通过第二个参数total保存了下来,因此没有必要再用一个完整的栈帧,去保存当前这轮递归计算的函数调用上下文了。这就引出了所谓“尾递归优化”的概念:

When a compiler detects a call that is tail recursive, it overwrites the current activation record instead of pushing a new one onto the stack. The compiler can do this because the recursive call is the last statement to be executed in the current activation; thus, there is nothing left to do in the activation when the call returns. Consequently, there is no reason to keep the current activation around. By replacing the current activation record instead of stacking another one on top of it, stack usage is greatly reduced, which leads to better performance in practice. Thus, we should make recursive functions tail recursive whenever we can.

https://www.oreilly.com/libra...

上述文字大意如下:

当(C语言)编译器检测到尾递归调用时,并不会创建新的栈帧并压入栈中,而是用新的栈帧覆盖掉当前处于激活状态的栈帧。编译器之所以能够这样做,是因为尾递归函数里,递归调用是当前栈帧里最后一个需要执行的函数调用。被覆盖掉的栈帧本身毫无用处,不需要再保留。采用栈帧覆盖,而不是新建栈帧的方式,极大程度上减少了栈帧的个数,提高了递归函数的执行性能。因此,应该尽可能地去尝试使用尾递归方式实现递归函数。

一个实际的性能比较例子:计算20的阶乘,二者的性能有巨大差异:普通递归实现需要10毫秒,而尾递归实现仅仅需要不到1毫秒的时间。

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

注意:一个递归函数能否用尾递归方式实现,和它能否享受运行时的尾递归优化,二者不是一回事,后者需要编译器的支持。

应用开发人员通过Scala提供的@tailrec注解,告诉编译器,对注解修饰的方法进行尾递归优化:

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

如果优化失败,或者被修饰的方法根本就不是一个尾递归函数,则编译器报错:

could not optimize @tailrec annotated method fibonacci: it contains a recursive call not in tail position

用ABAP实现尾递归版本的阶乘运算:

JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

至于ABAP编译器能否支持尾递归优化?我没有研究过,我只是觉得,尾递归优化并不能算是ABAP编译器必须实现的需求之一。

希望本文能帮助大家对尾递归优化这个概念有一个最基本的认识,感谢阅读。

ABAP专题

更多Jerry的原创文章,尽在:"汪子熙":
JavaScript, ABAP和Scala里的尾递归(Tail Recursion)

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Karen110 Karen110
3年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
3年前
Docker 部署SpringBoot项目不香吗?
  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。  !(http://dingyue.ws.126.net/2020/0920/b00fbfc7j00qgy5xy002kd200qo00hsg00it00cj.jpg)  2
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
笨鸟先飞
笨鸟先飞
Lv1
近乡情更怯,不敢问来人。
文章
4
粉丝
0
获赞
0