• 2005-08-10

    精通递归程序设计

    Tag:

    版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
    http://spearbaby.blogbus.com/logs/1349875.html

    递归的经典示例
    递归程序的基本步骤
    编写保证正确的程序
    尾部递归(Tail-recursive)函数
    结束语
    参考资料
    关于作者
    对本文的评价

     

    精通递归程序设计

     

     

    理解递归,以编写可维护的、一致的、保证正确的代码

    级别: 中级

    Jonathan Bartlett
    技术主管, New Media Worx
    2005
    7 11

    命令式语言开发人员并不经常使用递归这一工具,因为他们认为这样会较慢而且浪费空间,不过,作者通过示例表明,可以使用一些技术来尽减少或者避免这些问题。他介绍了递归以及递归程序设计模式的概念,研究了如何使用它们来编写保证正确的程序。示例是使用 Scheme C 编写的。

    计算机科学的新学生通常难以理解递归程序设计的概念。递归思想之所以困难,原因在于它非常像是循环推理(circular reasoning)。它也不是一个直观的过程;当我们指挥别人做事的时候,我们极少会递归地指挥他们。

    对刚开始接触计算机编程的人而言,这里有递归的一个简单定义:当函数直接或者间接调用自己时,则发生了递归。

    递归的经典示例
    计算阶乘是递归程序设计的一个经典示例。计算某个数的阶乘就是用那个数去乘包括 1 在内的所有比它小的数。例如,factorial(5) 等价于 5*4*3*2*1,而 factorial(3) 等价于 3*2*1

    阶乘的一个有趣特性是,某个数的阶乘等于起始数(starting number)乘以比它小一的数的阶乘。例如,factorial(5)  5 * factorial(4) 相同。您很可能会像这样编写阶乘函数:

    清单 1. 阶乘函数的第一次尝试

     

    int factorial(int n)

    {

       return n * factorial(n - 1);

    }

    不过,这个函数的问题是,它会永远运行下去,因为它没有终止的地方。函数会连续不断地调用 factorial。当计算到零时,没有条件来停止它,所以它会继续调用零和负数的阶乘。因此,我们的函数需要一个条件,告诉它何时停止。

    由于小于 1 的数的阶乘没有任何意义,所以我们在计算到数字 1 的时候停止,并返回 1 的阶乘(即 1)。因此,真正的递归函数类似于:

    清单 2. 实际的递归函数

     

    int factorial(int n)

    {

       if(n == 1)

       {

          return 1;

       }

       else

       {

          return n * factorial(n - 1);

       }

    }

    可见,只要初始值大于零,这个函数就能够终止。停止的位置称为 基线条件(base case。基线条件是递归程序的最底层位置,在此位置时没有必要再进行操作,可以直接返回一个结果。所有递归程序都必须至少拥有一个基线条件,而且必须确保它们最终会达到某个基线条件;否则,程序将永远运行下去,直到程序缺少内存或者栈空间。

    递归程序的基本步骤
    每一个递归程序都遵循相同的基本步骤:

    1.       初始化算法。递归程序通常需要一个开始时使用的种子值(seed value)。要完成此任务,可以向函数传递参数,或者提供一个入口函数,这个函数是非递归的,但可以为递归计算设置种子值。

    2.       检查要处理的当前值是否已经与基线条件相匹配。如果匹配,则进行处理并返回值。

    3.       使用更小的或更简单的子问题(或多个子问题)来重新定义答案。

    4.       对子问题运行算法。

    5.       将结果合并入答案的表达式。

    6.       返回结果。

    使用归纳定义
    有时候,编写递归程序时难以获得更简单的子问题。不过,使用 归纳定义的(inductively-defined)数据集 可以令子问题的获得更为简单。归纳定义的数据集是根据自身定义的数据结构 —— 这叫做 归纳定义(inductive definition

    例如,链表就是根据其本身定义出来的。链表所包含的节点结构体由两部分构成:它所持有的数据,以及指向另一个节点结构体(或者是 NULL,结束链表)的指针。由于节点结构体内部包含有一个指向节点结构体的指针,所以称之为是归纳定义的。

    使用归纳数据编写递归过程非常简单。注意,与我们的递归程序非常类似,链表的定义也包括一个基线条件 —— 在这里是 NULL 指针。由于 NULL 指针会结束一个链表,所以我们也可以使用 NULL 指针条件作为基于链表的很多递归程序的基线条件。

    链表示例
    让我们来看一些基于链表的递归函数示例。假定我们有一个数字列表,并且要将它们加起来。履行递归过程序列的每一个步骤,以确定它如何应用于我们的求和函数:

    1.       初始化算法。这个算法的种子值是要处理的第一个节点,将它作为参数传递给函数。

    2.       检查基线条件。程序需要检查确认当前节点是否为 NULL 列表。如果是,则返回零,因为一个空列表的所有成员的和为零。

    3.       使用更简单的子问题重新定义答案。我们可以将答案定义为当前节点的内容加上列表中其余部分的和。为了确定列表其余部分的和,我们针对下一个节点来调用这个函数。

    4.       合并结果。递归调用之后,我们将当前节点的值加到递归调用的结果上。

    下面是这个函数的伪代码和实际代码:

    清单 3. sum_list 程序的伪代码

     

     function sum_list(list l)

        is l null?

           yes - the sum of an empty list is 0 - return that

        data = head of list l

        rest_of_list = rest of list l

        the sum of the list is:

           data + sum_list(rest_of_list)

    这个程序的伪代码几乎与其 Scheme 实现完全相同。

    清单 4. sum_list 程序的 Scheme 代码

     

     (define sum-list (lambda (l)

        (if (null? l)

           0

           (let (

                 (data (car l))

                 (rest-of-list (cdr l)))

              (+ data (sum-list rest-of-list))))))

    对于这个简单的示例而言,C 版本同样简单。

    清单 5. sum_list 程序的 C

    随机文章:

    关于指针 2005-08-19

    收藏到:Del.icio.us