递归与尾递归
关于递归操作,相信大家都已经不陌生。简单地说,一个函数直接或间接地调用自身,是为直接或间接递归。例如,我们可以使用递归来计算一个单向链表的长度:
class Node:
def __init__(self, v):
self.v = v
self.n = None
编写一个递归的get_length方法:
def get_length_recursively(head):
if head == None:
return 0
else:
return get_length_recursively(head.n) + 1
在调用时,get_length_recursively方法会不断调用自身,直至满足递归出口。对递归有些了解的朋友一定猜得到,如果单项链表十分长,那么上面这个方法就可能会遇到栈溢出。这是由于每个线程在执行代码时,都会分配一定尺寸的栈空间,每次方法调用时都会在栈里储存一定信息(如参数、局部变量、返回地址等等),这些信息再少也会占用一定空间,成千上万个此类空间累积起来,自然就超过线程的栈空间了。不过这个问题并非无解,我们只需把递归改成如下形式即可(在这篇文章里我们不考虑非递归的解法):
def get_length_tail_recursively(head, acc):
if head == None:
return 0
else:
return get_length_recursively(head.n, acc + 1)
get_length_tail_recursively方法多了一个acc参数,acc的为accumulator(累加器)的缩写,它的功能是在递归调用时“积累”之前调用的结果,并将其传入下一次递归调用中——这就是get_length_tail_recursively方法与get_length_recursively方法相比在递归方式上最大的区别:get_length_recursively方法在递归调用后还需要进行一次“+1”,而get_length_tail_recursively的递归调用属于方法的最后一个操作。这就是所谓的“尾递归”。与普通递归相比,由于尾递归的调用处于方法的最后,因此方法之前所积累下的各种状态对于递归调用结果已经没有任何意义,因此完全可以把本次方法中留在堆栈中的数据完全清除,把空间让给最后的递归调用。这样的优化1便使得递归不会在调用堆栈上产生堆积,意味着即时是“无限”递归也不会让堆栈溢出。这便是尾递归的优势。
有些朋友可能已经想到了,尾递归的本质,其实是将递归方法中的需要的“所有状态”通过方法的参数传入下一次调用中。对于get_length_tail_recursively方法,我们在调用时需要给出acc参数的初始值:
get_length_tail_recursively(head, 0)
为了进一步熟悉尾递归的使用方式,我们再用著名的“菲波纳锲”数列作为一个例子。传统的递归方式如下:
def fibonacci_recursively(n):
if n < 2:
return n
else:
return fibonacci_recursively(n - 1) + fibonacci_recursively(n - 2)
而改造成尾递归,我们则需要提供两个累加器:
def fibonacci_tail_recursively(n, acc1, acc2):
if n == 0:
return acc1
else:
return fibonacci_tail_recursively(n - 1, acc2, acc1 + acc2)
于是在调用时,需要提供两个累加器的初始值:
fibonacci_tail_recursively(10, 0, 1)
尾递归与Continuation
Continuation,即为“完成某件事情”之后“还需要做的事情”。例如,在python中标准的with关键字和上下文管理器的调用方式,便是由__enter__方法和__exit__方法构成,这其实便是一种Continuation:在完成了__enter__方法之后,还需要调用__exit__方法。而这种做法,也可以体现在尾递归构造中。例如以下为阶乘方法的传统递归定义:
def factorial_recursively(n):
if n == 0:
return 1
else:
return factorial_recursively(n - 1) + 1
显然,这不是一个尾递归的方式,当然我们轻易将其转换为之前提到的尾递归调用方式。
def factorial_tail_recursively_inner(n, acc):
if n == 0:
return acc
else:
return factorial_tail_recursively_inner(n - 1, n * acc)
def factorial_tail_recursively(n):
return factorial_tail_recursively_inner(n, 1)
不过我们现在把它这样“理解”:每次计算n的阶乘时,其实是“先获取n - 1的阶乘”之后再“与n相乘并返回”,于是我们的FactorialRecursively方法可以改造成:
def factorial_continuation(n, continuation):
pass
def factorial_cps_recursively(n):
return factorial_continuation(n - 1, lambda r: r * n)
factorial_continuation方法的含义是“计算n的阶乘,并将结果传入continuation方法,并返回其调用结果”。于是,很容易得出,factorial_continuation方法自身便是一个递归调用:
def factorial_continuation(n, continuation):
return factorial_continuation(n - 1, lambda r: continuation(n * r))
factorial_continuation方法的实现可以这样表述:“计算n的阶乘,并将结果传入continuation方法并返回”,也就是“计算n - 1的阶乘,并将结果与n相乘,再调用continuation方法”。为了实现“并将结果与n相乘,再调用continuation方法”这个逻辑,代码又构造了一个匿名方法,再次传入factorial_continuation方法。当然,我们还需要为它补充递归的出口条件:
def factorial_continuation(n, continuation):
if n == 0:
return continuation(1)
return factorial_continuation(n - 1, lambda r: continuation(n * r))
很明显,factorial_continuation实现了尾递归。如果要计算n的阶乘,我们需要如下调用factorial_continuation方法,表示“计算10的阶乘,并将结果直接返回”:
factorial_continuation(n, lambda x: x)
最后总的代码段:
def factorial_continuation(n, continuation):
if n == 0:
return continuation(1)
return factorial_continuation(n - 1, lambda x: continuation(n * x))
def factorial_cps_recursively(n):
return factorial_continuation(n, lambda x: x)
再加深一下印象,大家是否能够理解以下计算“菲波纳锲”数列第n项值的写法?
def fibonacci_continuation(n, continuation):
if n < 2:
return continuation(n)
return fibonacci_continuation(n - 1,
lambda r1: fibonacci_continuation(n - 2,
lambda r2: continuation(r1 + r2)))
def fibonacci_cps_recursively(n):
return fibonacci_continuation(n, lambda x: x)
结束
在命令式编程中,我们解决一些问题往往可以使用循环来代替递归,这样便不会因为数据规模造成堆栈溢出。但是在函数式编程中,要实现“循环”的唯一方法便是“递归”,因此尾递归和CPS对于函数式编程的意义非常重大。了解尾递归,对于编程思维也有很大帮助,因此大家不妨多加思考和练习,让这样的方式为自己所用。