Scala中尾递归


    作为一个程序员,大家对递归应该都很熟悉,在《 数据结构与算法分析:C描述》书中,已打印链表为例,提到了尾递归,并指出了尾递归是对递归及其不当的使用,它指出虽然编译器会对递归进行自动优化,但是一般情况下还是不要使用尾递归。此外在Java中,递归的使用率也是很低,这可能是因为比起递归,循环在java中更容易实现,并且递归对于编写递归函数的人来说比较容易理解,但是对阅读的人来说可能不太容易理解。 但是这种情况在Scala中就完全不一样了,Scala语言支持函数式编程(从某种意义上来说鼓励我们使用递归代替循环)。我们都知道递归的效率不如循环,那么函数式编程语言中使用了如此的多的递归是如何保证效率的呢?答案就是这些语言会对尾递归进行优化。
    这里有几个问题,首先什么是尾递归?在scala中,如何合理的使用尾递归?以及在scala中使用尾递归需要注意哪些问题?
  1.   什么是尾递归?
    我们常说的递归函数指的是在函数中调用自身的函数。根据递归的调用方式,可以将递归分为首递归和尾递归。在首递归中,函数调用自身之后,再进行其他运算,因此在运行的时候需要不断的使用新的栈帧来保存函数中的临时变量,这种情况下,当递归的层次不是很多的时候,还不会出现问题,但是一旦敌对的层次很深的时候就很容易出现stack overflow的情况,这对程序而言往往是致命的。因此使用首递归并不安全。下面是一个首递归函数:
 
public class Node{
    private int value;
    private Node next;
    public Node(int value,Node next){
       this.value=value;
       this.next=next;
   }
   public int getValue(){
       return value;
   }
   public Node getNext(){
       return next;
   }
}
// 首递归函数
public static int getLength(Node head){
   if(head==null) return 0;
//  可以看到在递归调用getLength之后还进行一次求和计算,因此不是尾递归,在递归的过程中需要保持临时变量
   return getLength(head.getNext())+1

}

       而尾递归则完全不同,在尾递归函数中, 所有的计算都在调用之前完成(这一点非常重要),  因此函数可以在调用完成之后释放栈帧,因此不管递归的层次有多深都不会发生stack overflow的情况。将上述首递归函数改成尾递归函数的形式如下所示:
public static getLength(Node head){
   if(head==null)  return acc;
// 可以看到递归调用getLength之后整个函数的计算都已经完成,因此栈帧可以立即释放
   return getLength(head.getNext(),acc+1)

}
     
     注意上面的acc,我们可以认为是一个累加器(其实不一定是做加法,代表任何形式的一致积聚),用于积累之前调用的结果,这样调用的数据就不会被丢弃,这也是尾递归非常重要的一个特点。
    
2 、Scala中如何是使用尾递归?
    我们以典型的斐波那契数列为例,来看看scala中如何使用尾递归。
    首递归形式的斐波那契数列
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
    尾递归的形式如下所示:
def fibonacciTail(n:Int,acc1:Int,acc2:Int):Int={
    if(n<2)  acc2
    else  fibonacciTail(n-1,acc2,acc1+acc2)
}
    其调用过程如下所示:
fibonacciTailrec(5,0,1)
fibonacciTailrec(4,1,1)
fibonacciTailrec(3,1,2)
fibonacciTailrec(2,2,3)
fibonacciTailrec(1,3,5)
5
     
    其实在scala中使用尾递归最关键的地方还是需要理解递归函数需要返回的是什么,返回的结果应该如何积聚(本质上就是如何引入合适的累加器来积聚递归程序返回的结果)
     Scala对形式上严格的尾递归进行了优化,对于严格的尾递归,可以放心使用,不必担心性能问题。对于是否是严格尾递归,若不能自行判断, 可使用Scala提供的尾递归标注@scala.annotation.tailrec,这个符号除了可以标识尾递归外,更重要的是编译器会检查该函数是否真的尾递归,若不是,会导致如下编译错误。
could not optimize @tailrec annotated method fibonacci: it contains a recursive call not in tail position

3、在scala中使用尾递归需要注意哪些问题?
     由于scala本质上一种jvm语言,最后还是要编译为class文件,有jvm执行,因此其运行机制肯定会送到jvm的限制(这是其与其他纯函数式编程语言不通的地方), Scala对尾递归的优化很有限,它只能优化形式上非常严格的尾递归。下列的情况scala不会优化。
  • 递归不是直接调用,而是通过函数值。例如:
//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)
}

本文参考的资料如下:


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值