加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 综合聚焦 > 服务器 > 安全 > 正文

scala中的尾递归

发布时间:2020-12-16 09:47:51 所属栏目:安全 来源:网络整理
导读:递归 一个函数直接或间接的调用它自己,就是递归了。例如,递归计算阶乘: def factorial(n: Int): Int = { if ( n = 1 ) 1 else n * factorial(n- 1 )} 以上factorial方法,在n1时,需要调用它自身,这是一个典型的递归调用。 如果n=5,那么该递归调用的过

递归

一个函数直接或间接的调用它自己,就是递归了。例如,递归计算阶乘:

def factorial(n: Int): Int = {
    if( n <= 1 ) 1
    else n * factorial(n-1)
}

以上factorial方法,在n>1时,需要调用它自身,这是一个典型的递归调用。

如果n=5,那么该递归调用的过程大致如下:

factorial(5)
5 * factorial(4)
5 * (4 * factorial(3))
5 * (4 * (3 * factorial(2)))
5 * (4 * (3 * (2 * factorial(1))))
5 * (4 * (3 * (2 * 1)))
120

递归符合人们的思维方式,更容易理解,但是由于需要保持调用堆栈,效率比较低。在调用次数较多时,更经常耗尽内存,造成stack overflow。 因此,程序员们经常用递归实现最初的版本,然后对它进行优化,改写为循环以提高性能。

尾递归技术进入了人们的眼帘。

尾递归

尾递归是指递归调用是函数的最后一个语句,而且其结果被直接返回,这是一类特殊的递归调用。 由于递归结果总是直接返回,尾递归比较方便转换为循环,因此编译器容易对它进行优化。现在很多编译器都对尾递归有优化,程序员们不必再手动将它们改写为循环。

以上阶乘函数不是尾递归,因为递归调用的结果有一次额外的乘法计算,这导致每一次递归调用留在堆栈中的数据都必须保留。我们可以将它修改为尾递归的方式。

def factorialTailrec(n: BigInt,acc: BigInt): BigInt = {
    if(n <= 1) acc
    else factorialTailrec(n-1,acc * n)
}

现在我们再看调用过程,就不一样了,factorialTailrec每一次的结果都是被直接返回的。
以n=5为例,这次的调用过程如下。

factorialTailrec(5,1)
factorialTailrec(4,5) // 1 * 5 = 5
factorialTailrec(3,20) // 5 * 4 = 20
factorialTailrec(3,60) // 20 * 3 = 60
factorialTailrec(2,120) // 60 * 2 = 120
factorialTailrec(1,120) // 120 * 1 = 120120

以上的调用,由于调用结果都是直接返回,所以之前的递归调用留在堆栈中的数据可以丢弃,只需要保留最后一次的数据,这就是尾递归容易优化的原因所在,而它的秘密武器就是上面的acc,它是一个累加器(accumulator,习惯上翻译为累加器,其实不一定非是“加”,任何形式的积聚都可以),用来积累之前调用的结果,这样之前调用的数据就可以被丢弃了。

普通递归改写为尾递归

将普通的递归改写为尾递归,关键在于找到合适的累加器。下面我们以斐波那契数列为例,看看如何找到累加器。斐波那契数列,前两项为1,从第三项起,每一项都是它之前的两项和。这个定义就是天然的递归算法,如下。

def fibonacci(n: Int): Int = {
    if (n <= 2) 1
    else fibonacci(n - 1) + fibonacci(n - 2)
}

还是以n=5为例,看它的计算过程。

fibonacci(5)
fibonacci(4) + fibonacci(3)
(fibonacci(3) + fibonacci(2)) + (fibonacci(2) + fibonacci(1))
((fibonacci(2) + fibonacci(1)) + 1) + (1 + 1)
((1 + 1) + 1) + 2
5

以上显然不是尾递归。

我们发现斐波那契数的计算需要前两项的和,所以这里需要两个累加器。

假设较小的一个为acc1,较大的一个为acc2, 需要计算下一项时,将acc2赋值为新的的acc1’,而(acc1+acc2)赋值为acc2’。用类似于滚动数组的方式去累加。

调用堆栈中旧有的数据即可丢弃。以下是这个过程的演示。

n   0   1   2   3   4
F(n)    0   1   1   2   3
   acc1  acc2        
         acc1'=acc2 acc2'=acc1+acc2    
                     acc1''=acc2' acc2''=(acc1'+acc2') acc1'''=acc2''        acc2'''=acc1''+acc2''

根据上面的演示过程,可以写代码如下。

def fibonacciTailrec(n: Int,acc1: Int,acc2: Int): Int = {
    if (n < 2) acc2
    else fibonacciTailrec(n - 1,acc2,acc1 + acc2)
}

以上代码,直接返回递归的结果,因此是严格的尾递归,n=5时,调用过程如下。

fibonacciTailrec(5,1)
fibonacciTailrec(4,1,1)
fibonacciTailrec(3,2)
fibonacciTailrec(2,2,3)
fibonacciTailrec(1,3,5)
5

上述过程只是演示简单的改写递归的方法,事实上,关于累加器,有更普遍的规律可循,这里不再深入介绍。 对比上述普通递归和尾递归的效率,完整的代码如下。

def fibonacci(n: Int): Int = {
    if (n <= 2) 1
    else fibonacci(n - 1) + fibonacci(n - 2)
}

def fibonacciTailrec(n: Int,acc1 + acc2)
}

val list = List(20,30,40)
val sw = new Stopwatch
for (num <- list) {
    println("n = " + num)
    sw.start("Normal")
    val ret = fibonacci(num)
    println("F(n) = " + ret)
    sw.stop()

    sw.start("Tail")
    val retTail = fibonacciTailrec(num,0,1)
    println("FT(n) = " + retTail)
    sw.stop()
    println(sw.prettyPrint())
    println()
    sw.reset()
}

上述代码,某次执行输出的结果如下(处理器1.8GHz Intel Core i5)。

n = 20
F(n)  = 6765
FT(n) = 6765
Total time elapsed: 2(ms) -------------------------------------
 (ms) (%) Task name
 2 100.00 Normal
 0 0.00 TailRec -------------------------------------

n = 30
F(n)  = 832040
FT(n) = 832040
Total time elapsed: 3(ms) -------------------------------------
 (ms) (%) Task name
 3 100.00 Normal
 0 0.00 TailRec -------------------------------------

n = 40
F(n)  = 102334155
FT(n) = 102334155
Total time elapsed: 396(ms) -------------------------------------
 (ms) (%) Task name
 396 100.00 Normal
 0 0.00 TailRec -------------------------------------

完整例程,请参见07 Lecture 1.7 - Tail Recursion

Scala对尾递归的支持

Scala对形式上严格的尾递归进行了优化,对于严格的尾递归,可以放心使用,不必担心性能问题。对于是否是严格尾递归,若不能自行判断,可使用Scala提供的尾递归标注@scala.annotation.tailrec,这个符号除了可以标识尾递归外,更重要的是编译器会检查该函数是否真的尾递归,若不是,会导致如下编译错误。

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

局限

由于JVM的限制,对尾递归深层次的优化比较困难,因此,Scala对尾递归的优化很有限,它只能优化形式上非常严格的尾递归。也就是说,下列情况不在优化之列。

  • 如果尾递归不是直接调用,而是通过函数值。

比如以上阶乘的尾递归版本,如果我们改写为不是直接调用它,而是将函数赋值给func,编译器将不会认为它是尾递归。

//call function value will not be optimized
val func = factorialTailrec _
def factorialTailrec(n: BigInt,acc: BigInt): BigInt = {
    if(n <= 1) acc
    else func(n-1,acc*n)
}
  • 间接递归不会被优化

间接递归,指不是直接调用自身,而是通过其他的函数最终调用自身的递归。如下所示。

//indirect recursion will not be optimized
def foo(n: Int) : Int = {
    if(n == 0) 0;
    bar(n)
}
def bar(n: Int) : Int = {
    foo(n-1)
}

参考资料

  1. 【Scala】尾递归优化

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读