尾递归并按名称/值调用

时间:2019-06-04 00:24:57

标签: scala tail-recursion callbyname call-by-value

总体上学习Scala和函数式编程。在以下尾递归阶乘实现中:

def factorialTailRec(n: Int) : Int = {

    @tailrec
    def factorialRec(n: Int, f: => Int): Int = {
      if (n == 0) f else factorialRec(n - 1, n * f)
    }

    factorialRec(n, 1)
}

我想知道用 value 调用第二个参数相对于 name 调用第二个参数是否有好处(就像我所做的那样)。在第一种情况下,每个堆栈框架都会负担产品。在第二种情况下,如果我的理解是正确的,那么整个产品链将在第if ( n== 0)个堆栈帧处转移到案例n,因此我们仍然必须执行相同数量的乘法。不幸的是,这不是形式a ^ n的乘积,可以通过重复平方来以log_2n的步长进行计算,而是每次相差1的项的乘积。因此,我看不到任何优化最终产品的可能方法:它仍然需要O(n)项的乘法。

这是正确的吗?就复杂性而言,按值调用等同于按名称调用吗?

2 个答案:

答案 0 :(得分:2)

让我稍微扩展一下您已经在评论中告诉您的内容。 这就是编译器使用别名参数的方式:

@tailrec
def factorialTailRec(n: Int, f: => Int): Int = {
  if (n == 0) {
    val fEvaluated = f
    fEvaluated
  } else {
    val fEvaluated = f // <-- here we are going deeper into stack. 
    factorialTailRec(n - 1, n * fEvaluated)
  }
}

答案 1 :(得分:0)

通过实验,我发现通过按名称命名的调用,该方法变为... 非尾递归!我编写了以下示例代码,以递归方式比较阶乘尾部和非尾部阶乘:

 package example

 import scala.annotation.tailrec

 object Factorial extends App {

  val ITERS = 100000

  def factorialTailRec(n: Int) : Int = {
    @tailrec
    def factorialTailRec(n: Int, f: => Int): Int = {
      if (n == 0) f else factorialTailRec(n - 1, n * f)
    }
    factorialTailRec(n, 1)
  }

  for(i <-1 to ITERS) println("factorialTailRec(" + i + ") = " + factorialTailRec(i))


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

  for(i <-1 to ITERS) println("factorial(" + i + ") = " + factorial(i))

}

观察到内部tailRec函数按名称调用第二个参数。 @tailRec批注仍然不会引发编译时错误!

我一直在为ITERS变量使用不同的值,对于100,000,我收到一个... StackOverflowError

Stack overflow error in tail-recursive method

(由于Int的溢出,结果为零。)

所以我继续将factorialTailRec/2的签名更改为:

def factorialTailRec(n: Int, f: Int): Int 

即按值调用参数f。这次,main的运行factorialTailRec的部分完全可以正常运行,而factorial/1当然也以完全相同的整数崩溃了。

非常非常有趣。似乎在这种情况下,按名称进行调用会保留堆栈帧,因为需要对产品本身进行计算,一直返回到调用链。