开发软件时首先要考虑的是代码的可读性和可维护性。查看性能特征大多是过早的优化。
当它有助于编写高质量的代码时,没有理由不使用递归。
尾递归与正常循环的计算相同。看看这个简单的尾递归函数:
def gcd(a: Int, b: Int) = {
def loop(a: Int, b: Int): Int =
if (b == 0) a else loop(b, a%b)
loop(math.abs(a), math.abs(b))
}
它计算两个数字的最大公约数。一旦你知道了算法,它是如何工作的就很清楚了——用一个while循环编写它不会让它更清楚。相反,您可能会在第一次尝试时引入错误,因为您忘记将新值存储到变量之一a
或b
.
另一方面看到这两个函数:
def goRec(i: Int): Unit = {
if (i < 5) {
println(i)
goRec(i+1)
}
}
def goLoop(i: Int): Unit = {
var j = i
while (j < 5) {
println(j)
j += 1
}
}
哪一个更容易阅读?它们或多或少是相等的——由于基于 Scalas 表达式的性质,您为尾递归函数获得的所有语法糖都消失了。
对于递归函数,还有另一件事可以发挥作用:惰性求值。如果您的代码是惰性评估的,它可以是递归的,但不会发生堆栈溢出。看这个简单的功能:
def map(f: Int => Int, xs: Stream[Int]): Stream[Int] = f -> xs match {
case (_, Stream.Empty) => Stream.Empty
case (f, x #:: xs) => f(x) #:: map(f, xs)
}
大输入会崩溃吗?我不这么认为:
scala> map(_+1, Stream.from(0).takeWhile(_<=1000000)).last
res6: Int = 1000001
用 Scalas 尝试同样的方法List
会杀死你的程序。但是因为Stream
懒惰,所以这不是问题。在这种情况下,您还可以编写一个尾递归函数,但通常这并不容易。
有许多算法在迭代编写时并不清楚——一个例子是图的深度优先搜索。你想自己维护一个堆栈只是为了保存你需要返回的值吗?不,你不会,因为它容易出错并且看起来很丑(除了递归的任何定义之外 - 它也会调用迭代深度优先搜索递归,因为它必须使用堆栈并且“正常”递归必须使用堆栈以及 - 它只是对开发人员隐藏并由编译器维护)。
回到过早优化这一点,我听过一个很好的类比:当您遇到一个无法解决的问题时,Int
因为您的数字会变得很大并且很可能会溢出,然后不要切换到Long
因为这里很可能也会溢出。
对于递归,这意味着在某些情况下您可能会炸毁堆栈,但更有可能的是,当您切换到仅基于内存的解决方案时,您会收到内存不足错误。一个更好的建议是找到一种性能不会那么差的不同算法。
作为结论,尝试更喜欢尾递归而不是循环或正常递归,因为它肯定不会杀死你的堆栈。但是,当您可以做得更好时,请不要犹豫,做得更好。