Lisp.理解递归

来源:互联网 发布:java github 开源项目 编辑:程序博客网 时间:2024/05/20 09:47

学习递归的学生有时会被鼓励在纸上跟踪递归函数的所有调用。这种练习可能会有误导:程序员定义一个递归函数的时候,通常不会考虑因为调用它而产生的所有调用。如果有人非得以这一的方式来考虑程序,那么递归就是令人烦恼的,这可没有什么帮助。递归的好处是它的精确性,它让我们以从更加抽象的方式来观察算法。你可以不用考虑调用函数时所产生的所有的调用,就能判断出一个递归函数是否是正确的。


要探明一个递归函数是否按照我们的设想工作,你需要问的就是:它覆盖了所有情况吗?比如,这里是一个找出列表长度的递归函数:

(defun len (lst)

  (if (null lst)

    0

    (+ (len (cdr lst)) 1)))


我们可以通过确定两件事情来确保这个函数是正确的:

1. 它对列表长度为0的列表是可以正常工作的;

2. 假设它对于长度为n的列表能够正常工作,那么它对于长度为n+1的列表也能够正常工作;


如果我们建立起这两点,那么我们就知道这个函数对于所有可能的列表都是正确的。


我们的定义很显然符合第一点:如果lst是nil,函数立即返回0.现在我们假设函数对于长度为n的列表是可以正确工作的。那么我们给它一个长度为n+1的列表。我们的定义中说了:函数将返回这个列表的cdr的长度加上1.这个cdr就是一个长度为n的列表。按照我们的假设我们知道,它的len就是n。那么整个列表的len就是n+1.


这就是我们所有需要知道的东西了。理解递归的秘密非常像处理括号的秘密。你怎样知道哪个括号匹配哪个?你不需要那么做。你怎么可视化所有那些调用?你不需要那么做。


对于更加复杂的递归函数,可能有更多的情况需要考虑,但是过程是一样的。


我们这里第一个情况(这里是长度为0的列表)是基本情况(base case)。当一个递归函数不能按照预期工作时,通常是因为基本情况是错误的。完全丢失掉基本情况是一个很常见的错误,就像在下面的错误的member定义中的一样:

(defun our-member (obj lst)

  (if (eql (car lst) obj)

    lst

  (our-member obj (cdr lst))))

我们需要初始化空(null)测试来确保当到到列表尾部并且没有找到对应的元素的时候的时候递归能够结束。我们这个版本的程序将会在搜索不到对应值的时候进入无限循环。


能够判断一个递归函数是否正确仅仅是理解递归的一部分,另一部分是能够将你想做的用递归函数写出来。

原创粉丝点击