递归函数总结

对于递归一直有种恐惧感,看到循环多了就烦,其实没什么,多细心一下还是很有用的。

递归类似链表,总会调用一个和自己一样的东西,链表里是下一个节点,这儿就是函数本身。这儿关键要有基线条件,也就是最后终止的地方。

以下内容转载自ibm  http://www.ibm.com/developerworks/cn/linux/l-recurs.html

递归的经典示例

计算阶乘是递归程序设计的一个经典示例。计算某个数的阶乘就是用那个数去乘包括 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 代码
int sum_list(struct list_node *l)
{
   if(l == NULL)
      return 0;
    return l.data + sum_list(l.next);
}

您可能会认为自己知道如何不使用递归编写这个程序,使其执行更快或者更好。稍后我们会讨论递归的速度和空间问题。 在此,我们继续讨论归纳数据集的递归。

假定我们拥有一个字符串列表,并且想要知道某个特定的字符串是否包含在那个列表中。将此问题划分为更简单的问题的方法是,再次到单个的节点中去 查找。

子问题是这样:“搜索字符串是否与 这个节点 中的字符串相同?”如果是,则您就已经有了答案;如果不是,则更接近了一步。基线条件是什么? 有两个:

  • 如果当前节点拥有那个字符串,则那就是基线条件(返回“true”)。
  • 如果列表为空,则那也是基线条件(返回“false”)。

这个程序不是总能达到第一个基线条件,因为不是总会拥有正在搜索的字符串。不过,我们可以断言,如果程序不能达到第一个基线条件, 那么当它到达列表末尾时至少能达到第二个基线条件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值