递归是一种过程自调用的一种代码形式。
递归对于函数式编程来说,是极为重要的一门必须掌握的代码形式。
为什么这么说呢?因为,在函数式编程中,递归是实现代码多次重复运行的唯一形式。函数式编程只支持递归,不支持循环。
线性递归和树形递归
线性递归是指递归函数的代码中只存在一次对本身的调用。
线性递归在运行时,运行栈会一直向上增长,增长到头之后,就会一直向下减少,直到最后递归结束。也就是说,在线性递归的整个执行过程中,运行栈只发生一次伸缩。
需要注意的是,有些递归形式看起来像树形递归,实际上却是线性递归。
(define (factorial n)
(if (= n 1)
1
(* n (factorial (- n 1)))))
树形递归是指递归函数的代码中存在两次或两次以上的对本身的调用。最经典的例子就是斐波那契(Fibonacci)数列的计算。
; Fibonacci的尾递归写法,并且消除了重复计算,但是这只是特殊情况。
; 大多数情况下,树形递归是无法优化的。
; 比如,树形结构的遍历,你就得老老实实遍历所有结点,才能完成算法,没有捷径可走。
(define (fib n)
(fib-iter 1 0 n))
(define (fib-iter a b count)
(if (= count 0)
b
(fib-iter (+ a b) a (- count 1))))
尾递归
在线性递归中,有一种特殊的情况。递归调用是函数体中执行的最后一条语句。比如:
; 计算n的阶乘
(define (factorial n)
(define (iter product counter)
(if (> counter n)
product
(iter (* counter product)
(+ counter 1))))
(iter 1 1))
注意,一定要保证,递归调用是函数体中最后一条执行的语句,才能满足尾递归的条件。
使用尾递归的好处:循环和递归的转化; 空间和时间上的优化。
尾递归是一种最接近循环的递归形式。尾递归已经是递归向循环转换的最后一步,只要变换一下形式,尾递归就变成了循环。
由于尾递归在本质上已经等于循环,有些编译器(或者解释器)会对尾递归代码进行优化(Scheme实现强制要求支持尾递归),在发生递归调用的时候,不需要保存所有的中间结果,因而并不进行真正的压栈操作,而是如同处理循环一样直接运算。这种优化不会引起运行栈的一层层的增长,从而节省了空间。