Scala中的递归和尾部递归的实例教程

175 阅读5分钟

简介

递归在编程世界中是很常见的。如果你不知道递归,它意味着,通过将一个问题分解成更小的子问题来解决,直到你以琐碎的方式解决它。如果你看到一个方法用一个更小的输入子集来调用自己,你就可以很容易地发现递归。递归和不可变性是函数式编程的角落地带。正如我们前面所看到的,在使用循环时,我们倾向于使用积累结果或尝试循环的变量。然而,我们可以使用递归来解决这些变量,并将这些变量转换成Val。

在面向对象的编程中,我们通常不会观察到很多递归,但递归在函数式编程中是很常见的。递归提供了一种描述许多算法的自然方式,这里需要注意的一点是,当我们在scala中使用递归时,必须提供一个返回类型,因为编译器很难减少它。

让我们看一个递归的例子。我们将尝试用递归来找出两个数字的最大公因数。我将声明一个方法gcd,它将接受两个整数并返回一个整数。你必须仔细写出你的递归应该退出的条件,否则你就会陷入无限循环。

让我们用一些输入来测试一些方法。

scala > def gcd (a:Int, b:Int):Int = {
     | if(b == 0) a
     | else gcd (b, a%b)
     | }
def gcd (a: Int, b: Int): Int

scala > gcd(12,18)
val res0: Int = 6

关于递归的问题

递归涉及到建立一个调用栈,这可能是很昂贵的,普通的递归有一个主要问题,如果你不小心,它可能会炸毁你的栈。让我用一个例子来解释。

考虑一个关于数字之和的小例子。方法sum将尝试对所有的数字进行求和,直到输入。仔细观察我们是如何每次都减少一个数字并将其加入结果中的。在这里,每当我们再次调用sum时,我们会将输入的Val num留在堆栈中。因此,每次都会耗尽一点内存。

scala> def sum(num:Int):Int = {
     | if (num ==1 ) 1
     | else sum(num-1) + num
     | }
def sum(num: Int): Int

让我们试着朗读一些输入样本

scala> sum(999)
val res0: Int = 499500

而且进展顺利。现在,让我们再加一个9。

scala> sum(9999)
java.lang.StackOverflowError

  at sum(<console>:2)
  at sum(<console>:3)
  at sum(<console>:3)
  at sum(<console>:3)
  at sum(<console>:3)
  at sum(<console>:3)
  at sum(<console>:3)
  at sum(<console>:3)
  at sum(<console>:3)
  at sum(<console>:3)
  at sum(<console>:3)
  at sum(<console>:3)
  at sum(<console>:3)
  at sum(<console>:3)
  at sum(<console>:3)
  at sum(<console>:3)
  at sum(<console>:3)
   .
   .
   .
   .

你会看到,它已经炸毁了堆栈。

尾部递归

为了避免这样的问题,我们应该使用尾部递归。所以第一个问题是,什么是尾部递归。如果递归调用是函数执行的最后一个操作,并且在函数返回时不需要保存任何操作或数据,这就叫做尾部递归。如果堆栈框架上没有任何东西,编译器可以重复使用堆栈框架,并将其转换成一个循环。让我们通过一个例子来理解。

为了将递归转换为尾部递归,我们需要向我们的方法传递另一个输入,该方法将跟踪总和,每当我们降到1时将返回这个结果。仔细观察这一次,现在,我们没有在堆栈上留下任何残留物。

scala> def sum(num:Int, res:Int): Int= {
     | if (num == 1) res
     | else sum(num-1, res+num)
     | }
def sum(num: Int, res: Int): Int

现在让我们运行之前的一些输入,我们在之前的代码中已经给出了这些输入,出于好奇,再增加一些值。

scala> sum(99999,1)
val res2: Int = 704982704

scala> sum(999999,1)
val res3: Int = 1783293664

scala> sum(9999999,1)
val res4: Int = -2014260032

@tailrec 注释

到目前为止还不错。现在这是递归的小例子,我们可以看看它,并确定这个调用是否是尾部递归的,但是在现实生活的项目中,递归并不那么容易。那么,如何判断递归是尾部递归呢。在scala中,有一个简单的方法可以检查调用是否是尾部递归的,那就是使用尾部递归注解。

一旦你把这个注解放在你的方法调用上,scala就会检查你的方法是否是尾部递归的。如果不是,那么scala就会给你扔一个编译时错误。为了使用这个注解,我们需要导入scala.annotation包。

让我们看一个例子。首先,导入scala.annotation会把尾部递归放在某个不是尾部递归的方法上。

scala> import scala.annotation._
import scala.annotation._

scala> @tailrec
     | def sum(num:Int):Int = {
     | if (num == 1) 1
     | else sum(num-1) + num
     | }
                      
On line 4: error: could not optimize @tailrec annotated method sum: it contains a recursive call not in tail position

你可以直接看到scala向我们抛出了一个编译错误。让我们试一下,在我们声称是尾部递归的方法上添加尾部递归注解。

scala> import scala.annotation._
import scala.annotation._

scala> @tailrec
     | def sum(num:Int, res:Int):Int={
     | if (num == 1) res
     | else sum(num-1, res+num)
     | }
def sum(num: Int, res: Int): Int

这一次,它成功了。让我们通过给出一些数值来检查一下。

scala> sum(999,1)
val res0: Int = 499500

总结

这个话题会在你的代码中很多有用的地方用到。递归是一个重要的概念,而且很难掌握。所以,我想说,继续练习吧。这就是所有的基本知识,我现在脑海中与递归和尾部递归有关的最佳实践,任何初级水平的程序员都可以理解。语言并不重要。你可以用任何你想写的语言来写。

所以,把重点放在逻辑上。如果你发现有什么遗漏,或者你在任何一点上与我的观点不一致,请给我留言。我将很乐意讨论。

参考资料

www.baeldung.com/scala/tail-…

www.tutorialspoint.com/scala/recur…