递归函数应用

首先,我们来对比两个递归方法的求值步骤。

假设有方法gcd,用来计算两个数的最大公约数。下面是欧几里得算法的实现:

  1. def gcp(a: Int, b: Int): Int =
  2. if (b == 0) a else gcp(b, a % b)

gcp(14, 21)的求解过程如下:

  1. gcp(14, 21)
  2. if (21 == 0) 14 else gcd(21, 14 % 21)
  3. if (false) 14 else gcd(21, 14 % 21)
  4. gcd(21, 14 % 21)
  5. gcd(21, 14)
  6. if (14 == 0) 21 else gcp(14, 21 % 14)
  7. if (false) 21 else gcp(14, 21 % 14)
  8. gcp(14, 7)
  9. gcd(7, 14 % 7)
  10. gcd(7, 0)
  11. if (0 == 0) 7 else gcd(0, 7 % 0)
  12. if (true) 7 else gcd(0, 7 % 0)
  13. 7

再看数列阶乘问题:

  1. def factorial(n: Int): Int =
  2. if (n == 0) 1 else n * factorial(n - 1)

factorial(4)的求解过程如下:

  1. factorial(4)
  2. if (4 == 0) 1 else 4 * factorial(4 - 1)
  3. 4 * factorial(3)
  4. 4 * (3 * factorial(2))
  5. 4 * (3 * (2 * (factorial(1)))
  6. 4 * (3 * (2 * (1 * factorial(0)))
  7. 4 * (3 * (2 * (1 * 1))
  8. 24

上面两种递归的执行顺序有什么区别呢?先看最大公约数的递归求解过程,每次递归调用的时候不需要记录其他的值,而是直接调用递归函数;再看阶乘的递归求解过程,我们发现每次递归调用的结果还需要乘上一个n才能得到结果,也就是说每次递归调用都需要维系调用之前的状态。

这就是递归的两种不同形式,首递归(head recursion)和尾递归(tail recursion)。上面的阶乘递归解法就是一种首递归,而最大公约数的球解则是一个尾递归。

对于首递归,递归函数调用之后,后续还有计算,因此在执行过程中需要不断的使用新的栈帧来保存临时状态。首递归在递归层数较少的情况下不会有问题,但是由于需要消耗栈帧来保存临时变量,当递归层数达到一定数量的时候会导致stack overflow的异常。

对于尾递归,所有的计算都在递归调用之前完成,因此不需要保存临时状态,也就是说完全可以复用当前栈帧。如果复用了栈帧,那么不管递归多少层都不会发生stack overflow。这种复用栈帧的优化本质其实就是迭代计算,可以说优化后的尾递归是迭代的一种表达方式,其执行效率和迭代一样。

广义上来说,只要一个递归函数的最后一个操作只由调用函数组成(不管是其他函数,还是其他函数),栈帧都可以复用,这这种递归形式都可以叫做尾递归。而Scala中并不是对所有的尾递归都做了优化,只有那些满足严格尾递归形式的递归函数才会被优化而服用栈帧。

尾递归栈帧

为了不引起歧义,这里的尾递归指的是scala中能够进行栈帧复用优化的递归。我们先来看看非尾递归函数的堆栈,定义如下非尾递归函数headRecStackFrame,并调用headRecStackFrame(10),:

  1. @tailrec
  2. def headRecStackFrame(n: Int): Int =
  3. if (n == 0) throw new Exception("boom!")
  4. else n * headRecStackFrame(n - 1)
  5. headRecStackFrame(5)

我们得到的异常堆栈如下:

  1. java.lang.Exception: boom!
  2. at cc.databus.tailrecur.TailRecursionStackFrame$.headRecStackFrame(TailRecursionStackFrame.scala:13)
  3. at cc.databus.tailrecur.TailRecursionStackFrame$.headRecStackFrame(TailRecursionStackFrame.scala:14)
  4. at cc.databus.tailrecur.TailRecursionStackFrame$.headRecStackFrame(TailRecursionStackFrame.scala:14)
  5. at cc.databus.tailrecur.TailRecursionStackFrame$.headRecStackFrame(TailRecursionStackFrame.scala:14)
  6. at cc.databus.tailrecur.TailRecursionStackFrame$.headRecStackFrame(TailRecursionStackFrame.scala:14)
  7. at cc.databus.tailrecur.TailRecursionStackFrame$.headRecStackFrame(TailRecursionStackFrame.scala:14)
  8. at cc.databus.tailrecur.TailRecursionStackFrame$.main(TailRecursionStackFrame.scala:26)
  9. at cc.databus.tailrecur.TailRecursionStackFrame.main(TailRecursionStackFrame.scala)

可以看到6个headRecStackFrame调用的栈帧。

再来看看尾递归函数的堆栈。定义尾递归函数tailRecStackFrame,并调用tailRecStackFrame(5):

  1. @tailrec
  2. def tailRecStackFrame(n: Int): Int =
  3. if (n == 0) throw new Exception("boom!")
  4. else tailRecStackFrame(n - 1)
  5. tailRecStackFrame(5)

得到如下异常堆栈:

  1. java.lang.Exception: boom!
  2. at cc.databus.tailrecur.TailRecursionStackFrame$.tailRecStackFrame(TailRecursionStackFrame.scala:9)
  3. at cc.databus.tailrecur.TailRecursionStackFrame$.main(TailRecursionStackFrame.scala:18)
  4. at cc.databus.tailrecur.TailRecursionStackFrame.main(TailRecursionStackFrame.scala)

这里我们看到只有一个tailRecStackFrame的调用栈帧。

对比headRecStackFrametailRecStackFrame的异常堆栈,可以明显发现tailRecStackFrame尾递归函数服用了调用栈帧。

Scala中的尾递归

前面提到,Scala中的只对严格形式的尾递归进行了优化,对于严格形式的尾递归,我们可以放心使用,不用担心栈溢出的问题。为了帮助我们判断一个递归函数是否是满足scala的尾递归优化策略,scala提供了@tailrec注解,这个注解一方面可以方便我们识别尾递归,同事编译器会自动检测该函数是否是尾递归,若不是,会导致如下编译错误:

  1. Error:(15, 10) could not optimize @tailrec annotated method headRecStackFrame: it contains a recursive call not in tail position
  2. else n * headRecStackFrame(n - 1)

在scala中,下面情况下scala不会优化:

  • 通过函数值实现递归
  • 不是直接调用递归函数,而是嵌套在其他函数中调用

通过函数值实现递归的例子:

  1. object NonTailRecursionExample {
  2. val fun = funcValRecursion _
  3. def funcValRecursion(a: Int): Int =
  4. if(a == 0) throw new Exception("Boom!")
  5. else fun(a - 1)
  6. def main(args: Array[String]): Unit = {
  7. funcValRecursion(3)
  8. }
  9. }

嵌套函数调用实现递归的:

  1. object NonTailRecursionExample {
  2. def anotherFunc(a: Int): Int =
  3. nestFunRecursion(a)
  4. def nestFunRecursion(a: Int): Int =
  5. if (a == 0) throw new Exception("Boom!")
  6. else anotherFunc(a - 1)
  7. def main(args: Array[String]): Unit = {
  8. nestFunRecursion(3)
  9. }
  10. }

Scala尾递归的更多相关文章

  1. 【Scala】尾递归优化

    以递归方式思考 递归通过灵巧的函数定义,告诉计算机做什么.在函数式编程中,随处可见递归思想的运用.下面给出几个递归函数的例子: object RecursiveExample extends App{ ...

  2. Scala Tail Recursion (尾递归)

    Scala对尾递归进行了优化,甚至提供了专门的标注告诉编译器需要进行尾递归优化.不过这种优化仅限于严格的尾递归,间接递归等情况,不会被优化. 尾递归的概念 递归,大家都不陌生,一个函数直接或间接的调用 ...

  3. Scala进阶之路-尾递归优化

    Scala进阶之路-尾递归优化 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 递归调用有时候能被转换成循环,这样能节约栈空间.在函数式编程中,这是很重要的,我们通常会使用递归方法来 ...

  4. Scala 经典的模式匹配和尾递归

    Scala 经典的模式匹配和尾递归 package io import java.io.{BufferedWriter, File, FileWriter} import java.text.Simp ...

  5. Thinking in scala (4)----阶乘与尾递归

    code1: object factorial{ def main(args:Array[String])={ println(factorial(args(0).toInt)) } def fact ...

  6. scala通过尾递归解析提取字段信息

    一.背景 获取数据中以“|”作为字段间的分隔符,但个别字段中数据也是以“|”作为分隔符.因此,在字段提取时需要保护数据完整性. 二.实现 1.数据以“|”分隔,可以采用递归方式迭代解析.通过尾递归方式 ...

  7. scala实战学习-尾递归函数

    求 $$ \Sigma\sideset{^b_a}f(x) $$ object sumfunc{ def sum(f: Int => Int)(a: Int)(b:Int): Int = { @ ...

  8. Scala 的确棒

    我的确认为计算机学院应该开一门 Scala 的语言课程. 在这篇文章中,我会讲述为什么我会有这样的想法,在此之前,有几点我想要先声明一下: 本文无意对编程语言进行评比,我要讲述的主体是为什么你应该学习 ...

  9. Scala HandBook

    目录[-] 1.   Scala有多cool 1.1.     速度! 1.2.     易用的数据结构 1.3.     OOP+FP 1.4.     动态+静态 1.5.     DSL 1.6 ...

随机推荐

  1. shell清除文件内容脚本

    先来代码: [root@localhost Qingchu]# cat Qingchu.sh #!/bin/bash #描述: #作者:孤舟点点 #版本:1.0 #创建时间:-- :: PATH=/b ...

  2. [资讯]同济D&I出品 | 绝对是史上最萌的机器人教具!!!

      机器人小曼……" 3D打印.激光切割等先进工艺. Anibot中包含三种不同的动物形象:小鸡安妮.小鹿安娜.猫头鹰安迪.孩子们通过对各个元器件的学习及编程确保它们幸福成长在阳光氤氲的森林 ...

  3. ajax学习相关

    choices参数 当存在某个字段的值只有有限的几个时,如性别(只有男.女),婚否(已婚.未婚)等,这种情况,我们可以用数字来代替这些中文字符,可以减轻数据库的负担,这时,就需要用到choices参数 ...

  4. Java學習筆記(基本語法)

    本文件是以學習筆記的概念為基礎,用於自我的複習紀錄,不過也開放各位的概念指證.畢竟學習過程中難免會出現觀念錯誤的問題.也感謝各位的觀念指證. 安裝JDK 在Oracle網站中找自己系統的JDK下載位置 ...

  5. 生死状:苹果VS他的供应商

    据知情人士透露,苹果已经组建了代号为Titan的汽车团队,并招募了数百名员工,准备进入汽车领域,iCar大有呼之欲出之势.事实上,苹果CEO蒂姆-库克早在去年就参观了宝马位于莱比锡的核心工厂,学习如何 ...

  6. 使用Lucene.Net做一个简单的搜索引擎-全文索引

    Lucene.Net Lucene.net是Lucene的.net移植版本,是一个开源的全文检索引擎开发包,即它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎. ...

  7. ip修改成域名

    将ip修改成域名,这样的话可以使程序变得更加健壮,别人不能直接看见你的ip地址. 后来总结下分享给大家.首先找到hosts文件的位置,这个文件是系统dns默认查找的文件. windows 系统:C:\ ...

  8. Mysql报错:Authentication .....Reading from the stream has failed

    连接Mysql5.7版本的数据库出现报错:Authentication to host '171.13.164.***' for user 'root' using method 'mysql_nat ...

  9. C#开发BIMFACE系列36 服务端API之:回调机制

    系列目录     [已更新最新开发文章,点击查看详细] 在<C# 开发 BIMFACE 系列文章>中介绍了模型转换.模型对比接口.这2个功能接口比较特殊,发起请求后,逻辑处理是在BIMFA ...

  10. 位运算基础(Uva 1590,Uva 509题解)

    逻辑运算 规则 符号 与 只有1 and 1 = 1,其他均为0 & 或 只有0 or 0 = 0,其他均为1 | 非 也就是取反 ~ 异或 相异为1相同为0 ^ 同或 相同为1相异为0,c中 ...