上一篇文章《谈递归时,我们在谈论什么》,我们讲解了递归调用的过程是怎么样的。还有这篇文章:《面试官的一次面试经历分享》里谈到了尾递归的问题。我们今天就来讲一下尾递归的事情。
在scala编程中,或者说在函数式编程中,是鼓励大家使用递归,而不是循环来解决问题。这是因为循环会引入变量,而变量是函数式编程中被视为洪水猛兽一样的存在。那是另外一个话题,在我的scala课程中会详细讲解。这里就不多做解释了。我们还是聚集本节课的重点,如何写一个高性能的递归。
先来看一下什么是尾递归。如果调用者在调用一个递归函数并且取得返回值以后,不再进行其他的操作,而是直接将这个值返回出去,那么这就是一个尾递归。拿面试经历分享的那个例子来说,
int fib(int n) {
if (n < 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
在调用了fib(n-1)和fib(n-2)之后,还要进行一次求和计算。那么,这就不是一个尾递归。而我的另一个写法:
public
在递归调用了fib函数以后,没有其他任何的计算,直接就将调用得到的返回值return出去了。这就是一种尾递归的写法。
尾递归有什么好处呢?通过对递归调用过程的分析,我们知道了,每次调用一个函数的时候,系统都会重新开辟一个与这个函数相对应的栈帧。可以想象,如果递归调用的深度比较大,栈帧会开辟很多,一来是浪费空间,二来性能也必然会下降很多(有很多读写内存操作)。相反,如果使用循环,则只在一个函数栈空间里,不会开辟更多的空间,所以使用循环,性能要远远好于递归。
但是函数式编程语言中(例如Haskell),没有for, while这样的关键字,也没有变量这种东西,所有的循环操作都要靠递归来实现。这就带来了性能上的矛盾。为了解决这个问题,编译器大多提供一种能力:在把源码编译成机器码的时候,用户编写的尾递归程序会被转变成循环。这是编译器自动去做的,用户并不感知。我们拿scala来验证一下。
先看简单递归版本:
def
会打出来很多调用栈,因为要执行多次递归。例如:
java
可以看到,在运行过程中,创建了很多fib函数的栈。尾递归版本的结果又如何呢?
def
这次的执行结果,只有一次打印:
java
也就是说,这个递归并没有真正展开成普通的递归,而是转成循环在执行了。整个过程没有引入一个变量。完美。
搞明白了尾递归是什么,我们才完成了一半,还要看一下,是不是所有的循环都很方便地改成尾递归呢?我们看第二部分:
循环改成尾递归
把循环改成尾递归,有一个通用的技巧,那就是把循环的写法中,需要使用变量的地方,都改成递归参数。在计算时需要使用的变量并不会变少,但我们却不再操心变量的赋值。再以fibnacci函数举例,循环版本是这么写的:
int fib(int n) {
int a = 1, b = 0;
for (int i = 0; i < n; i++) {
int t = b;
b = a;
a = a + t;
}
return a;
}
就是说,循环过程中,我们需要a, b 两个变量,那我们就把这两个变量转为递归函数的参数就可以了。
同样的办法,我们再来改写一下,面试经历分享这篇文章中power函数。先写它的循环版本:
public
我们看到,在递归的过程中,使用了三个变量result, t 和 i,那么我们的尾递归函数在定义的时候就带上这三个参数即可。
def pow_helper(n : Int, r : Double, t : Double, i : Int) : Double = {
if (n < i) {
r
}
else {
if ((n & i) > 0)
pow_helper(n, r * t, t * t, i << 1);
else
pow_helper(n, r, t * t, i << 1);
}
}
def pow_2(m : Double, n : Int) : Double = {
return pow_helper(n, 1.0, m, 1);
}
由于计算过程中,m不再起作用,所以我们在定义pow_helper的时候,就把m省略掉了。对比一下,循环实现和尾递归实现,你会发现,result, t, i 这三个变量只是换了一种形式,以前是局部变量,现在则是函数参数。
这是把循环改成尾递归的通用方法,如果循环本身逻辑就很复杂,改写起来可能也会变得很复杂。这个要根据具体情况来具体分析。
另外,一个问题的尾递归写法也有很多种写法,比如power函数,还可以这么写:
// 这是一种完全的尾递归:即递归函数返回以后,不需要再做任何计算了。
// 编译器可以将这个函数转成完全
// 的循环计算。而不是递归计算。
def pow2(x : Double, y : Double, n : Int) : Double = {
if (n == 0)
y
else if ((n & 1) == 1)
pow2 (x, x * y, n - 1);
else
pow2 (x * x, y, n / 2);
}
看懂上面的程序,做为今天的家庭作业吧。