Lisp.使用递归(Using Recursion)

来源:互联网 发布:少女前线枪娘数据 编辑:程序博客网 时间:2024/05/16 14:54

相比于其它语言,在Lisp中递归扮演着更加重要的角色。这其中可能有三个主要的原因:

1. 函数式变成。递归算法引入副作用的可能性看上去比较小。

2. 递归数据结构。Lisp的隐式指针使得创建递归定义的数据结构变得简单。最普遍的就是列表:一个列表或者是nil,或者是一个cdr为列表的cons。

3. 优雅。Lisp程序员非常关心他们的程序是否漂亮,而递归算法通常比它们的递归版本更加优雅。


学生一开始的时候会经常感到递归难以理解。但是,你不必考虑一个递归函数的所有的调用来判断它是否正确。


如果你想写一个递归函数也是一样的。如果你可以描述一个问题的递归解决方案,那么将你的方案翻译成代码也是很直接的。要使用递归来解决问题,你必须要做两件事情:

1. 你必须了解怎样通过将问题分解成有限数量的相似的,但是更小的问题。

2. 你必须了解怎样解决最小版本的问题——基础情况(base case)。


如果你做了这些,你就做到了。你知道一个有限的问题最终会被解决,因为每个递归都使得问题更小,并且最小的问题使用了有限数量的步骤。


比如,下面这个找出一个列表长度的递归算法,我们在每次递归的时候都会找到小一些列表的长度:

1. 一般情况下,一个列表的长度是一个列表的cdr的长度加上1

2. 一个空列表的长度是0。

当这种描述被翻译成代码时,基本情况必须先出现;但是当构想递归函数的时候,人们通常是从一般情况开始的。


前面的描述显式得描述了找到一个列表的长度的方法。当你定义一个递归函数的时候,你必须确认你分解问题的方式会带来更小的子问题。一个普通列表的cdr对于length来说会产生一个较小的子问题,但是一个循环列表的cdr不会。


这里有另外两个递归算法的例子。注意在第二个中,我们在每次递归中都将我呢提分成了两个更小的问题:

member:某个东西如果是列表的第一个元素,或者是这个列表的cdr中member,那么这个东西就是这个列表的member。没有东西是一个空列表的member。

copy-tree:一个cons的copy-tree,是一个由它的的car的copy-tree和它的的cdr的copy-tree组成的cons。一个原子的copy-tree是它自身。


一旦你能够以这种方式来描述一个算法,写出递归定义就是很小的一步了。


一些算法可以很自然地以这种方式表达出来,而有的却不是这样。如果不使用递归来定义ou-copy-tree,那么你可能费尽九牛二虎之力来定义它。另一方面,迭代版本的show-squares相比于递归版本来说,迭代版本的实现可能更容易理解。有时候,在你尝试写代码之前,哪种方式更自然一些可能没有那么明显。


如果你关心效率,有其它两个方面需要考虑。第一,尾递归。在有一个好的编译器的情况下,尾递归和循环在速度上应该没有什么差异。然而,如果你将要走出很远才能完成一个函数尾递归,那么使用迭代可能更好一些。


另外一点需要铭记在心的是,显而易见的递归算法并不一直是最有效率的。经典的例子是Fibonacci函数,它是以递归的方式定义的,

1. Fib(0) = Fib(1) = 1

2. Fin(n) = Fib(n-1) + Fib(n-2)

但是直译过来的形式

(defun fib (n)  (if (<= n 1)    1  (+ (fib (- n 1))     (fib (- n 2)))))
确实令人毛骨悚然的没有效率。同样的计算一次又一次地被重复。如果你计算(fib 10),那么函数就会计算(fib 9) 和(fib 8)。但是计算(fib 9),它必须又一次计算(fib 8),如此等等。


这里有一个迭代版本的函数,它和上面的函数产生同样的结果:


(defun fib (n)  (do ((i n (- i 1))    (f1 1 (+ f1 f2))    (f2 1 f1))  ((<= i 1) f1)))

迭代版本并不那么清晰,但是更加有效率。在实践中这种事情发生的可能性多大呢?很少见——这就是为什么所有的教科书都使用相同的例子——但是这是我们应该了解的。

原创粉丝点击