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)。基线条件是递归程序的

最底层位置,在此位置时没有必要再进行操作,可以直接返回一个结果。所有递归程序都必须至少拥有一个基线条件,而且

必须确保它们最终会达到某个基线条件;否则,程序将永远运行下去,直到程序缺少内存或者栈空间。

回页首

递归程序的基本步骤

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

初始化算法。递归程序通常需要一个开始时使用的种子值(seed

value)。要完成此任务,可以向函数传递参数,或者提供一个入口函数, 这个函数是非递归的,但可以为递归计算设置种子值。

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

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

对子问题运行算法。

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

返回结果。

使用归纳定义

有时候,编写递归程序时难以获得更简单的子问题。

不过,使用 归纳定义的(inductively-defined)数据集

可以令子问题的获得更为简单。归纳定义的数据集是根据自身定义的数据结构 ——

这叫做 归纳定义(inductive definition)。

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

NULL,结束链表)的指针。 由于节点结构体内部包含有一个指向节点结构体的指针,所以称之为是归纳定义的。

使用归纳数据编写递归过程非常简单。注意,与我们的递归程序非常类似,链表的定义也包括一个基线条件 —— 在这里是 NULL 指针。

由于 NULL 指针会结束一个链表,所以我们也可以使用 NULL 指针条件作为基于链表的很多递归程序的基线条件。

链表示例

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

以确定它如何应用于我们的求和函数:

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

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

使用更简单的子问题重新定义答案。我们可以将答案定义为当前节点的内容加上列表中其余部分的和。为了确定列表其余部分的和,

我们针对下一个节点来调用这个函数。

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

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

清单 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”)。

这个程序不是总能达到第一个基线条件,因为不是总会拥有正在搜索的字符串。不过,我们可以断言,如果程序不能达到第一个基线条件,

那么当它到达列表末尾时至少能达到第二个基线条件。

清单 6. 确定给定的列表中是否包含给定字符串的 Scheme 代码

(define is-in-list

(lambda (the-list the-string)

;;Check for base case of "list empty"

(if (null? the-list)

#f

;;Check for base case of "found item"

(if (equal? the-string (car the-list))

#t

;;Run the algorithm on a smaller problem

(is-in-list (cdr the-list) the-string)))))

这个递归函数能很好地工作,不过它有一个主要的缺点 ——

递归的每一次迭代都要为 the-string

传递 相同的值。传递额外的参数会增加函数调用的开销。

不过,我们可以在函数的起始处设置一个闭包(closure),以使得不再必须为每一个调用都传递那个字符串:

清单 7. 使用闭包的搜索字符串的 Scheme 程序

(define is-in-list2

(lambda (the-list the-string)

(letrec

(

(recurse (lambda (internal-list)

(if (null? internal-list)

#f

(if (equal? the-string (car internal-list))

#t

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值