Scala尾递归
递归函数应用
首先,我们来对比两个递归方法的求值步骤。
假设有方法gcd,用来计算两个数的最大公约数。下面是欧几里得算法的实现:
def gcp(a: Int, b: Int): Int =
if (b == 0) a else gcp(b, a % b)
gcp(14, 21)的求解过程如下:
gcp(14, 21)
if (21 == 0) 14 else gcd(21, 14 % 21)
if (false) 14 else gcd(21, 14 % 21)
gcd(21, 14 % 21)
gcd(21, 14)
if (14 == 0) 21 else gcp(14, 21 % 14)
if (false) 21 else gcp(14, 21 % 14)
gcp(14, 7)
gcd(7, 14 % 7)
gcd(7, 0)
if (0 == 0) 7 else gcd(0, 7 % 0)
if (true) 7 else gcd(0, 7 % 0)
7
再看数列阶乘问题:
def factorial(n: Int): Int =
if (n == 0) 1 else n * factorial(n - 1)
factorial(4)的求解过程如下:
factorial(4)
if (4 == 0) 1 else 4 * factorial(4 - 1)
4 * factorial(3)
4 * (3 * factorial(2))
4 * (3 * (2 * (factorial(1)))
4 * (3 * (2 * (1 * factorial(0)))
4 * (3 * (2 * (1 * 1))
24
上面两种递归的执行顺序有什么区别呢?先看最大公约数的递归求解过程,每次递归调用的时候不需要记录其他的值,而是直接调用递归函数;再看阶乘的递归求解过程,我们发现每次递归调用的结果还需要乘上一个n才能得到结果,也就是说每次递归调用都需要维系调用之前的状态。
这就是递归的两种不同形式,首递归(head recursion)和尾递归(tail recursion)。上面的阶乘递归解法就是一种首递归,而最大公约数的球解则是一个尾递归。
对于首递归,递归函数调用之后,后续还有计算,因此在执行过程中需要不断的使用新的栈帧来保存临时状态。首递归在递归层数较少的情况下不会有问题,但是由于需要消耗栈帧来保存临时变量,当递归层数达到一定数量的时候会导致stack overflow的异常。
对于尾递归,所有的计算都在递归调用之前完成,因此不需要保存临时状态,也就是说完全可以复用当前栈帧。如果复用了栈帧,那么不管递归多少层都不会发生stack overflow。这种复用栈帧的优化本质其实就是迭代计算,可以说优化后的尾递归是迭代的一种表达方式,其执行效率和迭代一样。
广义上来说,只要一个递归函数的最后一个操作只由调用函数组成(不管是其他函数,还是其他函数),栈帧都可以复用,这这种递归形式都可以叫做尾递归。而Scala中并不是对所有的尾递归都做了优化,只有那些满足严格尾递归形式的递归函数才会被优化而服用栈帧。
尾递归栈帧
为了不引起歧义,这里的尾递归指的是scala中能够进行栈帧复用优化的递归。我们先来看看非尾递归函数的堆栈,定义如下非尾递归函数headRecStackFrame,并调用headRecStackFrame(10),:
@tailrec
def headRecStackFrame(n: Int): Int =
if (n == 0) throw new Exception("boom!")
else n * headRecStackFrame(n - 1)
headRecStackFrame(5)
我们得到的异常堆栈如下:
java.lang.Exception: boom!
at cc.databus.tailrecur.TailRecursionStackFrame$.headRecStackFrame(TailRecursionStackFrame.scala:13)
at cc.databus.tailrecur.TailRecursionStackFrame$.headRecStackFrame(TailRecursionStackFrame.scala:14)
at cc.databus.tailrecur.TailRecursionStackFrame$.headRecStackFrame(TailRecursionStackFrame.scala:14)
at cc.databus.tailrecur.TailRecursionStackFrame$.headRecStackFrame(TailRecursionStackFrame.scala:14)
at cc.databus.tailrecur.TailRecursionStackFrame$.headRecStackFrame(TailRecursionStackFrame.scala:14)
at cc.databus.tailrecur.TailRecursionStackFrame$.headRecStackFrame(TailRecursionStackFrame.scala:14)
at cc.databus.tailrecur.TailRecursionStackFrame$.main(TailRecursionStackFrame.scala:26)
at cc.databus.tailrecur.TailRecursionStackFrame.main(TailRecursionStackFrame.scala)
可以看到6个headRecStackFrame调用的栈帧。
再来看看尾递归函数的堆栈。定义尾递归函数tailRecStackFrame,并调用tailRecStackFrame(5):
@tailrec
def tailRecStackFrame(n: Int): Int =
if (n == 0) throw new Exception("boom!")
else tailRecStackFrame(n - 1)
tailRecStackFrame(5)
得到如下异常堆栈:
java.lang.Exception: boom!
at cc.databus.tailrecur.TailRecursionStackFrame$.tailRecStackFrame(TailRecursionStackFrame.scala:9)
at cc.databus.tailrecur.TailRecursionStackFrame$.main(TailRecursionStackFrame.scala:18)
at cc.databus.tailrecur.TailRecursionStackFrame.main(TailRecursionStackFrame.scala)
这里我们看到只有一个tailRecStackFrame的调用栈帧。
对比headRecStackFrame和tailRecStackFrame的异常堆栈,可以明显发现tailRecStackFrame尾递归函数服用了调用栈帧。
Scala中的尾递归
前面提到,Scala中的只对严格形式的尾递归进行了优化,对于严格形式的尾递归,我们可以放心使用,不用担心栈溢出的问题。为了帮助我们判断一个递归函数是否是满足scala的尾递归优化策略,scala提供了@tailrec注解,这个注解一方面可以方便我们识别尾递归,同事编译器会自动检测该函数是否是尾递归,若不是,会导致如下编译错误:
Error:(15, 10) could not optimize @tailrec annotated method headRecStackFrame: it contains a recursive call not in tail position
else n * headRecStackFrame(n - 1)
在scala中,下面情况下scala不会优化:
- 通过函数值实现递归
- 不是直接调用递归函数,而是嵌套在其他函数中调用
通过函数值实现递归的例子:
object NonTailRecursionExample {
val fun = funcValRecursion _
def funcValRecursion(a: Int): Int =
if(a == 0) throw new Exception("Boom!")
else fun(a - 1)
def main(args: Array[String]): Unit = {
funcValRecursion(3)
}
}
嵌套函数调用实现递归的:
object NonTailRecursionExample {
def anotherFunc(a: Int): Int =
nestFunRecursion(a)
def nestFunRecursion(a: Int): Int =
if (a == 0) throw new Exception("Boom!")
else anotherFunc(a - 1)
def main(args: Array[String]): Unit = {
nestFunRecursion(3)
}
}
Scala尾递归的更多相关文章
- 【Scala】尾递归优化
以递归方式思考 递归通过灵巧的函数定义,告诉计算机做什么.在函数式编程中,随处可见递归思想的运用.下面给出几个递归函数的例子: object RecursiveExample extends App{ ...
- Scala Tail Recursion (尾递归)
Scala对尾递归进行了优化,甚至提供了专门的标注告诉编译器需要进行尾递归优化.不过这种优化仅限于严格的尾递归,间接递归等情况,不会被优化. 尾递归的概念 递归,大家都不陌生,一个函数直接或间接的调用 ...
- Scala进阶之路-尾递归优化
Scala进阶之路-尾递归优化 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 递归调用有时候能被转换成循环,这样能节约栈空间.在函数式编程中,这是很重要的,我们通常会使用递归方法来 ...
- Scala 经典的模式匹配和尾递归
Scala 经典的模式匹配和尾递归 package io import java.io.{BufferedWriter, File, FileWriter} import java.text.Simp ...
- Thinking in scala (4)----阶乘与尾递归
code1: object factorial{ def main(args:Array[String])={ println(factorial(args(0).toInt)) } def fact ...
- scala通过尾递归解析提取字段信息
一.背景 获取数据中以“|”作为字段间的分隔符,但个别字段中数据也是以“|”作为分隔符.因此,在字段提取时需要保护数据完整性. 二.实现 1.数据以“|”分隔,可以采用递归方式迭代解析.通过尾递归方式 ...
- scala实战学习-尾递归函数
求 $$ \Sigma\sideset{^b_a}f(x) $$ object sumfunc{ def sum(f: Int => Int)(a: Int)(b:Int): Int = { @ ...
- Scala 的确棒
我的确认为计算机学院应该开一门 Scala 的语言课程. 在这篇文章中,我会讲述为什么我会有这样的想法,在此之前,有几点我想要先声明一下: 本文无意对编程语言进行评比,我要讲述的主体是为什么你应该学习 ...
- Scala HandBook
目录[-] 1. Scala有多cool 1.1. 速度! 1.2. 易用的数据结构 1.3. OOP+FP 1.4. 动态+静态 1.5. DSL 1.6 ...
随机推荐
- http,tcp,udp的报文格式
http请求报文与响应报文:https://blog.csdn.net/qq_26565861/article/details/80969960 tcp与udp报文:https://www.cnblo ...
- from PIL import image报错
python中import PIL可以,但是from PIL import Image就报错? ’‘ 大家在安装pillow的时候,可能会安装成功,但是当运行from pIL import image ...
- 爬虫入门-使用python写简单爬虫
从第一章到上一章为止,基本把python所有的基础点都已经包括了,我们有控制逻辑的关键字,有内置数据结构,有用于工程需要的函数和模块,又有了标准库和第三方库,可以写正规的程序了. python可以做非 ...
- python 使用 UTF-8 编码
题记 一般我喜欢用 utf-8 编码,在 python 怎么使用呢? 使用utf-8 文字 在 python 源码文件中用 utf-8 文字.一般会报错,如下: File "F:\works ...
- nginx: [emerg] unknown directive “ ” in /usr/local/nginx/conf/vhost/XXX.conf:53报错处理
开发同事发给我一小段nginx配置,加到服务器上之后,执行nginx -s reload时,出现报错: nginx: [emerg] unknown directive “ ” in /usr/loc ...
- 微软亚洲研究院开源图数据库GraphView
我们很高兴地宣布,由微软亚洲研究院系统算法组开发的图数据库GraphView通过GitHub平台开源.GraphView是一款中间件软件,方便用户使用关系数据库SQL Server 或Azure SQ ...
- u-boot的环境变量详解
u-boot的环境变量 u-boot的环境变量是使用u-boot的关键,它可以由你自己定义的,但是其中有一些也是大家经常使用,约定熟成的,有一些是u-boot自己定义的,更改这些名字会出现错 ...
- 天哪!毫无思绪!令人感到恐惧的数学(水题?)(TOWQs)
这道题的题目描述灰常简单,第一眼看以为是一道十分水的题目: 但是!!!(我仔细一看也没有发现这背后隐藏着可怕的真相~) 下面给出题目描述: 给出一个整数x,你可以对x进行两种操作.1.将x变成4x+3 ...
- Docker深入浅出系列 | 单机Nginx+Springboot实战
目录 Nginx+Springboot实战 前期准备 实战目标 实战步骤 创建Docker网络 搭建Mysql容器 搭建额度服务集群 搭建Nginx服务 验证额度服务 附录 Nginx+Springb ...
- 关于js传送json到.net后台处理
这里的内容好像跟标题不太符合,应该是如何实现将请求得到的结果作为另一个请求的请求参数,方法就是使用json处理配合全局变量进行处理 今天做项目遇到以下情景,页面请求获得一个list数据,然后要将得到的 ...