我碰巧看到马丁·奥德斯基先生在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)
}
他的代码比我的有什么好处?哪个更有效率,为什么?
答案 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
,它允许进行一些优化以减少嵌套调用的数量。