两个循环的差异。哪个更有效,为什么?

时间:2015-03-11 10:26:48

标签: scala recursion

我碰巧看到马丁·奥德斯基先生在Scala上发现Coursera tutorial。要找到数字的阶乘,这就是我写的函数。

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

然而,当我看到视频时,奥德斯基先生表达了这样的话

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

  loop(1, n)
}

他的代码比我的有什么好处?哪个更有效率,为什么?

3 个答案:

答案 0 :(得分:4)

其他人对尾部递归是正确的,但我是一个教人钓鱼的忠实粉丝。让这两个版本的时间

object factorial extends App {

  // we need a routine to time
  def time[A](name: String, expr: => A): Unit = {
    val start = System.nanoTime()
    val result = expr
    val time = System.nanoTime() - start

    println(s"$name took $time")
  }

  // non tail recurisve factorial
  def factorial1(n: Int): Int = {
    if (n == 0) 1
    else n * factorial1(n - 1)
  } 

  // tail recurisve factorial
  def factorial2(n : Int): Int = {
    def loop(acc: Int, n: Int): Int = {
      if (n == 0) acc
      else loop(acc * n, n-1)
    }

    loop(1, n)
  }

  //Some driver code
  val n = 200
  time("factorial1", factorial1(n))
  time("factorial2", factorial2(n))

}

在我的盒子上,我得到了

$ scala factorial
factorial1 took 56000
factorial2 took 20000

(请注意,如果我认真对待这种情况,我不会只运行每个版本一次)

现在让我们看一下相关的字节码

$ javap -private -c factorial$.class
Compiled from "factorial.scala"
public final class factorial$ implements scala.App {

但只是相关的一点。首先,factorial1

public int factorial1(int);
  Code:
     0: iload_1
     1: iconst_0
     2: if_icmpne     9
     5: iconst_1
     6: goto          18
     9: iload_1
    10: aload_0
    11: iload_1
    12: iconst_1
    13: isub
    14: invokevirtual #120                // Method factorial1:(I)I
    17: imul
    18: ireturn

第14行递归调用factorial1

现在是factorial2及其内循环方法

public int factorial2(int);
  Code:
     0: aload_0
     1: iconst_1
     2: iload_1
     3: invokespecial #125                // Method loop$1:(II)I
     6: ireturn

private final int loop$1(int, int);
  Code:
     0: iload_2
     1: iconst_0
     2: if_icmpne     7
     5: iload_1
     6: ireturn
     7: iload_1
     8: iload_2
     9: imul
    10: iload_2
    11: iconst_1
    12: isub
    13: istore_2
    14: istore_1
    15: goto          0

请注意,内循环方法(循环$ 1)不会递归调用自身。相反,指令15执行"转到0"直接跳回到内循环的顶部。这是尾递归的魔力。我们不需要保留一大堆中间结果,我们只需要一点点信息来计算下一步。

答案 1 :(得分:2)

两者都是递归,但只有第二个尾递归,因此是尾调用优化的候选者。如果应用此优化,则递归将转换为正常循环(这意味着不会调用函数,因此会删除调用函数的成本)。

在第一个版本中,最后一个操作是乘法,因此编译器无法应用尾调用优化

第二个版本使用累加器,因此最后一个操作是递归(递归函数调用)。因此,可以应用尾部调用优化

如果添加@tailrec注释,编译器将检查是否真正应用尾部调用优化

Java编译器不应用尾调用优化。至于Scala,可以看到“与TCO”和“没有TCO”的区别here

答案 2 :(得分:1)

如您所见,两种解决方案都使用递归。但是第二个示例使用tail recursion,它允许进行一些优化以减少嵌套调用的数量。

More info about tail recursion