Scala 尾部递归与头部经典递归

Scala 尾部递归与头部经典递归,scala,recursion,Scala,Recursion,听Scala课程和解释我经常听到:“但在实际代码中,我们使用的不是递归,而是尾部递归” 这是否意味着在我的真实代码中我不应该使用递归,而是使用非常类似于循环的尾部递归,并且不需要史诗般的短语“为了理解递归,首先需要理解递归” 实际上,考虑到您的堆栈。。您更可能使用类似循环的尾部递归 我错了吗?“经典”递归是否只适合教育目的,让你的大脑回到大学的过去 或者,尽管如此,我们还是有地方可以使用它。。其中递归调用的深度小于X(其中X表示堆栈溢出限制)。或者我们可以从经典递归开始编码,然后,担心有一天你的

听Scala课程和解释我经常听到:“但在实际代码中,我们使用的不是递归,而是尾部递归”

这是否意味着在我的真实代码中我不应该使用递归,而是使用非常类似于循环的尾部递归,并且不需要史诗般的短语“为了理解递归,首先需要理解递归”

实际上,考虑到您的堆栈。。您更可能使用类似循环的尾部递归

我错了吗?“经典”递归是否只适合教育目的,让你的大脑回到大学的过去

或者,尽管如此,我们还是有地方可以使用它。。其中递归调用的深度小于X(其中X表示堆栈溢出限制)。或者我们可以从经典递归开始编码,然后,担心有一天你的堆栈会被破坏,应用几次重构,使其在重构领域发挥更大的作用

问题:一些真实的示例,您可能会在真实的代码中使用/使用“经典的头部”递归,这些代码可能还没有重构为尾部的

尾部递归==循环

您可以接受任何循环,并将其表示为尾部递归调用

背景:在纯FP中,一切都必须产生某种价值<代码>而scala中的循环不会产生任何表达式,只会产生副作用(例如,更新某些变量)。它的存在只是为了支持来自命令式背景的程序员。Scala鼓励开发人员重新考虑用递归替换
while
循环,这总是会产生一些值

因此,根据Scala:递归是新的迭代

然而,前面的语句有一个问题:虽然“常规”递归代码更容易阅读,但它会带来性能损失,并带有溢出堆栈的固有风险。另一方面,tail recursive代码永远不会导致堆栈溢出(至少在Scala*中),性能将与循环相同(事实上,我确信Scala会将所有tail recursive调用转换为普通的旧迭代)

回到问题上来,坚持“常规”递归没有什么错,除非:

  • 计算大数时使用的算法(堆栈溢出)
  • 尾部递归带来了显著的性能提升

  • 在开发软件时,首先要考虑的是代码的可读性和可维护性。查看性能特征大多是过早的优化

    当递归有助于编写高质量代码时,没有理由不使用它

    尾部递归与普通循环的计数相同。看看这个简单的尾部递归函数:

    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))
    }
    
    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)
    }
    
    它计算两个数字的最大公约数。一旦你知道了这个算法,它的工作原理就很清楚了——用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
      }
    }
    
    它会因为大量输入而崩溃吗?我不这么认为:

    scala> map(_+1, Stream.from(0).takeWhile(_<=1000000)).last
    res6: Int = 1000001
    

    scala>map(+1,Stream.from(0).takeWhile(p>如果您不是在处理线性序列,那么尝试编写尾部递归函数来遍历整个集合是非常困难的。在这种情况下,为了可读性/可维护性,您通常只使用普通递归

    一个常见的例子是遍历二叉树数据结构。对于每个节点,您可能需要在左侧和右侧子节点上重复出现。如果要尝试递归地编写这样一个函数,首先访问左侧节点,然后访问右侧节点,则需要维护某种辅助数据结构来跟踪所有rem获取需要访问的正确节点。但是,您可以通过使用堆栈实现同样的功能,并且它将更具可读性

    这方面的一个例子是:


    递归有两种基本类型:

  • 头部递归
  • 尾部递归
  • 在头部递归中,函数进行递归调用,然后执行更多的计算,例如,可能使用递归调用的结果。在尾部递归函数中,所有计算都首先发生,递归调用是最后发生的事情

    这一区别的重要性不会让你感到惊讶,但它非常重要!想象一个尾部递归函数。它运行。它完成所有计算。作为它的最后一个动作,它准备进行递归调用。在这一点上,堆栈框架的用途是什么?根本没有。我们不需要局部变量nymore,因为我们已经完成了所有的计算。我们不需要知道我们使用的是哪个函数,因为我们只需要重新输入同一个函数。Scala,在尾部递归的情况下,可以消除新堆栈帧的创建,只需重新使用当前堆栈帧。。堆栈永远不会变深,不会变深不管递归调用了多少次,这就是使尾部递归在Scala中特别的巫毒

    让我们看看这个例子

     def factorial1(n:Int):Int =
         if (n == 0) 1 else n * factorial1(n -1)
    
    
     def factorial2(n:Int):Int = {
          def loop(acc:Int,n:Int):Int =
               if (n == 0) 1 else loop(acc * n,n -1)
    
         loop(1,n)  
      } 
    
    顺便说一句,有些语言通过将尾部递归转换为迭代而不是通过操纵堆栈来达到类似的目的

    这对head递归不起作用。你明白为什么吗?想象一个head递归函数。首先它做一些工作,然后进行递归调用,然后再做一些工作。我们不能在进行递归调用时重复使用当前堆栈帧。递归调用完成后,我们需要堆栈帧信息.它有我们的局部变量,包括递归调用返回的结果(如果有)

     def factorial1(n:Int):Int =
         if (n == 0) 1 else n * factorial1(n -1)
    
    
     def factorial2(n:Int):Int = {
          def loop(acc:Int,n:Int):Int =
               if (n == 0) 1 else loop(acc * n,n -1)
    
         loop(1,n)  
      }