首页计算机计算机软件计算机语言与程序设计 06 递归.ppt

06 递归.ppt

页数:41页文件大小:969.0 KB来源: 计算机语言与程序设计ppt课件

本课件围绕递归算法展开,先讲或结点、与结点、多关联点与结点等概念,画出求n!的与或结点图并说明调用与返回的递归过程,再比较递归与递推的差异,随后以哈诺塔与青蛙过河两道经典例题演示如何把问题分解为可直接求解的结点,给出递归公式与完整C语言参考程序。

第六讲递归章节封面页,标示本讲系统讲解递归算法及其实现
递归导入页,说明递归在算法设计中的地位并给出求n!的递归定义与边界
或结点说明页,定义依不同条件取两种不同值的或结点及其符号
查看本课件的文字版(共 41 页,供检索与复制)

第1页:第六讲递归封面

本页为第六讲 递归章节封面,本讲系统讲解递归概念、与或结点图、哈诺塔与青蛙过河等递归问题。

第2页:递归算法导入

递归算法在可计算性理论中占有重要地位,它是算法设计的有力工具,对于拓展编程思路非常有用。就递归算法而言并不涉及高深数学知识,只不过初学者要建立起递归概念不十分容易。先从最简单的例子导入——用递归算法求 n!

定义:函数 fact(n) = n! fact(n-1) = (n-1)! 则有 fact(n) = n * fact(n-1) 已知 fact(1) = 1

第3页:或结点

为了表述得直观清晰,定义两个结点:或结点与与结点。A 为“或结点”,A 依不同条件会有两种不同的取值 B 或 C。

第4页:多取值或结点

如果有多于 2 种取值,可用下图表示:条件为 Z1, Z2, …, Zn,取值为 B 或 C, … 或 G。

第5页:与结点

  1. 与结点:与结点要涂黑,相关联的 B 与 C 之间要用弧线连起来。A 为与结点,A 的最终取值为 C 结点的值,但为了求得 C 的值,得先求出 B 结点的值,C 是 B 的函数。

第6页:求n!的与或图

仍以求 n! 为例画出与或图:A 为或结点;B 为直接可解结点,值为 1;C 为与结点,当 n>1 时 A 的取值即 C 的值,而 C 的值即 E 的值;为了求得 E 的值,需要先求出 D 的值,D 值 fact(n-1) 乘以 n 即为 E 的值。

第7页:多关联点与结点

与结点可能有多个相关联的点,这时可描述为下图:A 结点的值最终为 D 的值,但为了求 D 需先求 B 和 C。从图上看先求左边的点才能求最右边的点的值,我们约定最右边 D 点的值就是 A 结点的值。

第8页:3!的与或结点图

下面我们以 3!为例来画与或结点图,目的是体会递归的含义:

C = 1 D = 2*C = 2 B = D = 2 E = 3*B = 3*2 = 6 A = E = 6

第9页:调用与返回的递归示意图

本页画出了调用和返回的递归示意图,展示 fact 函数层层调用自身、到达边界后再逐层返回的过程。

第10页:递归过程的形象解释

从图可以想象:欲求 fact(3),先要求 fact(2);要求 fact(2) 先求 fact(1)。就象剥一颗圆白菜,从外向里一层层剥下来,到了菜心遇到 1 的阶乘其值为 1,到达了递归的边界。然后再用 fact(n)=n*fact(n-1) 这个普遍公式,从里向外倒推回去得到 fact(n) 的值。为了把这个问题说得再透彻一点,另画了如下流程图。

第11页:递归流程图

本页以流程图形式展示求阶乘时的递归调用与返回流程,源文件未给出更多文字说明。

第12页:递归过程改画图

为了形象地描述递归过程,将上图改画成下图。

第13页:递归与递推的比较

在这个图中“内层”与“外层”有着相同的结构,它们之间“你中有我,我中有你”,呈现相互依存的关系。为讲清递归概念,将递归与递推做一比较,仍以求阶乘为例。递推是从已知的初始条件出发,逐次去求所需要的阶乘值:

初始条件 fact(1) = 1 fact(2) = 2*fact(1) = 2 fact(3) = 3*fact(2) = 6

第14页:递归算法的优越性

递推相当于从菜心“推到”外层。而递归算法的出发点不放在初始条件上,而放在求解的目标上,从所求的未知项出发逐次调用本身的求解过程,直到递归的边界(即初始条件)。

就本例而言读者会认为递归算法可能多余、费力不讨好,但许多实际问题不可能或不容易找到显而易见的递推关系,这时递归算法就表现出明显优越性:符合人的思维方式,逻辑性强,可读性好、易于理解。下面举一个尽人皆知的例子——哈诺(Hanoi)塔问题。

第15页:哈诺塔故事

故事:相传在古代印度的 Bramah 庙中,有位僧人整天把三根柱子上的金盘倒来倒去,原来他是想把 64 个一个比一个小的金盘从一根柱子移到另一根柱子上去。移动过程恪守规则:每次只允许移动一只盘,且大盘不得落在小盘上面。

有人会觉得这很简单,真的动手移盘就会发现:如以每秒移动一只盘子计算,将 64 只盘子移完所需时间约为 5800 亿年。

第16页:一只盘子的移法

怎样编写这种程序?从思路上还是先从最简单的情况分析起,搬一搬看,慢慢理出思路。

  1. 在 A 柱上只有一只盘子,假定盘号为 1,这时只需将该盘从 A 搬至 C,一次完成,记为 move 1 from A to C

第17页:两只盘子的移法

  1. 在 A 柱上有二只盘子,1 为小盘,2 为大盘:第1步将 1 号盘从 A 移至 B,这是为了让 2 号盘能移动;第2步将 2 号盘从 A 移至 C;第3步再将 1 号盘从 B 移至 C
move 1 from A to B; move 2 from A to C; move 3 from B to C;

第18页:三只盘子的移法

  1. 在 A 柱上有 3 只盘子,从小到大分别为 1、2、3 号:
  • 第1步将 1 号盘和 2 号盘视为一个整体,先将二者作为整体从 A 移至 B,给 3 号盘创造一次移至 C 的机会,记为 move(2, A, C, B)
  • 第2步将 3 号盘从 A 移至 C,一次到位,记为 move 3 from A to C
  • 第3步把处于 B 上作为一个整体的 2 只盘子再移至 C,记为 move(2, B, A, C)

所谓借助是什么意思,等这件事做完了不言自明。

第19页:三盘移动过程

本页以图示方式展示 A 柱上有 3 只盘子时的整体转移过程,源文件未给出更多文字说明。

第20页:整体移动的分解

  1. 从题目的约束条件看,大盘上可以随便摞小盘,相反则不允许。在将 1 号和 2 号盘当整体从 A 移至 B 的过程中,move(2, A, C, B) 实际上分解为以下三步:
第(1).1步:move 1 from A to C; 第(1).2步:move 2 from A to B; 第(1).3步:move 1 from C to B;

同样,3 号盘一旦到了 C,move(2, B, A, C) 也要分解为三步:

第(3).1步:move 1 from B to A; 第(3).2步:move 2 from B to C; 第(3).3步:move 1 from A to C;

第21页:搬移函数的定义

  1. 看 move(2, A, C, B) 是说要将 2 只盘子从 A 搬至 B,但没有 C 是不行的,因为第(1).1步就要将 1 盘从 A 移到 C,给 2 盘创造条件,然后再把 1 盘从 C 移至 B,由此明白借助 C 的含义。因此在构思搬移过程的参量时,要把 3 个柱子都用上
  2. 定义搬移函数 move(n, A, B, C),物理意义是将 n 只盘子从 A 经 B 搬到 C

考虑到前面研究过的 (1)(2)(3) 步,可把搬移过程用与或结点图表示。

第22页:哈诺塔的三步分解

这里用与或结点的含义是分解为 (1)(2)(3) 步,这 3 步是相关的、相互依存的,而且是有序的,从左至右执行:

  • (1) move(n-1, A, C, B):将上面的 n-1 只盘子作为一个整体从 A 经 C 移至 B
  • (2) 输出 n:A to C:将 n 号盘从 A 移至 C,是直接可解结点
  • (3) move(n-1, B, A, C):将上面的 n-1 只盘子作为一个整体从 B 经 A 移至 C

第23页:递归定义的展开

这里显然是一种递归定义。当解 move(n-1, A, C, B) 时又可想到将其分解为 3 步:

  • 第1步:将上面的 n-2 只盘子作为一个整体从 A 经 B 到 C,move(n-2, A, B, C)
  • 第2步:第 n-1 号盘子从 A 直接移至 B,即 n-1: A to B
  • 第3步:再将上面的 n-2 只盘子作为一个整体从 C 经 A 移至 B,move(n-2, C, A, B)

下面仍以 3 只盘子为例画出递归的与或图。

第24页:倒置的递归树

这个图很象一颗倒置着的树:结点 move(3, A, B, C) 是树根,与结点是树的分枝,叶子都是直接可解结点。

第25页:哈诺塔递归树

本页以图形方式给出哈诺塔递归分解树,源文件未给出更多文字说明。

第26页:哈诺塔递归结构

本页继续以图形方式展示哈诺塔递归结构与盘号移动顺序,源文件未给出更多文字说明。

第27页:哈诺塔参考程序

#include <stdio.h> int step=1;void move(int , char ,char,char);void main() { int n; printf("请输入盘数n=" ); scanf("%d",&n); printf("在3根柱子上移%d只盘的步骤为:\n",n); move(n,'a','b','c'); } void move(int m, char p, char q, char r) { if (m==1) { printf("[%d] move 1# from %c to %c\n", step, p, r); step=step+1; } else { move(m-1,p,r,q); printf("[%d] move %d# from %c to %c\n", step, m, p, r); step=step+1; move(m-1,q,p,r); } }

第28页:哈诺塔小结

本页以图示方式收束哈诺塔问题的递归实现,源文件未给出更多文字说明。

第29页:青蛙过河问题

讨论问题——青蛙过河。该题是 2000 年全国青少年信息学奥林匹克的一道试题,叙述如下:

一条小溪尺寸不大,青蛙可以从左岸跳到右岸。左岸有一石柱 L,面积只容一只青蛙落脚,右岸也有一石柱 R,同样只容一只。有一队青蛙从小到大用 1,2,…,n 编号,初始时只能趴在左岸的石头 L 上,按号一个落一个,小的落在大的上面,不允许大的在小的上面。

小溪中有 S 个石柱、y 片荷叶:溪中柱子上允许一只青蛙落脚,如有多只同样要求按号大的在下小的在上;荷叶只允许一只青蛙落脚,不允许多只在其上;右岸石柱 R 与左岸石柱 L 一样允许多个青蛙按号叠落。

青蛙从左岸 L 跳走后不允许再跳回来;从左岸 L 跳至右岸 R,或从溪中荷叶、溪中石柱跳至右岸 R 上的青蛙也不允许再离开。问:已知溪中有 S 根石柱和 y 片荷叶,最多能跳过多少只青蛙?

第30页:青蛙过河的解题思路

这题看起来较难,但认真分析、理出思路就可化难为易。

  1. 简化问题,探索规律:先从个别再到一般,善于对多个因素作分解,孤立出一个一个因素来分析,化难为易
  2. 定义函数 Jump(S,y)——最多可跳过河的青蛙数,其中 S 为河中柱子数、y 为荷叶数

第31页:河中无柱子的情况

  1. 先看简单情况,河中无柱子:S=0,Jump(0,y)。当 y=1 时 Jump(0,1)=2:河中有一片荷叶可以过两只青蛙,起始时 L 上有两只青蛙,1# 在 2# 上面
  • 第一步:1# 跳到荷叶上
  • 第二步:2# 从 L 直接跳至 R 上
  • 第三步:1# 再从荷叶跳至 R 上

第32页:归纳出Jump(0,y)=y+1

当 y=2 时 Jump(0,2)=3:河中有两片荷叶时可以过 3 只青蛙。起始时 1#、2#、3# 落在 L 上:

  • 第一步:1# 从 L 跳至叶1;第二步:2# 从 L 跳至叶2;第三步:3# 从 L 直接跳至 R
  • 第四步:2# 从叶2跳至 R;第五步:1# 从叶1跳至 R

采用归纳法:Jump(0,y)=y+1,意思是河中没有石柱时,过河的青蛙数仅取决于荷叶数,数目是荷叶数+1。

第33页:S=1且y=1的跳法

再看 Jump(S, y)。先看一个最简单情况:S=1,y=1,从图上看出需要 9 步,跳过 4 只青蛙:

  1. 1# 青蛙从 L → Y
  2. 2# 青蛙从 L → S
  3. 1# 青蛙从 Y → S
  4. 3# 青蛙从 L → Y
  5. 4# 青蛙从 L → R
  6. 3# 青蛙从 Y → R
  7. 1# 青蛙从 S → Y
  8. 2# 青蛙从 S → R
  9. 1# 青蛙从 Y → R

第34页:青蛙过河过程表

表一:S=1、y=1 时各时刻青蛙的位置(t=0~9)
时刻L1L2L3L4S1S2YR1R2R3R4
t=01234———————
t=1234———1————
t=234———21————
t=334——12—————
t=44———123————
t=5————1234———
t=6————12—34——
t=7————2—134——
t=8——————1234—
t=9———————1234

说明:源文件该页内容以图示与粘连字符形式给出(L1~L4、S1~S2、Y、R1~R4 各位置按时刻 0~9 排列),上表按前后页所述的九步跳法还原,供对照原图核对。

第35页:一分为二的系统分解

为把过河过程描述得更清楚,给出了表 1。表中 L1 L2 L3 L4 表示左岸石柱上落在一起的青蛙的高度位置,L1 在最上面、L4 在最下面;同理 R1 R2 R3 R4 也是如此。对水中石柱 S 分成两个高度位置 S1、S2;对荷叶 Y 无须分层,因为它只允许一只青蛙落在其上。

  • t=0 为初始时刻,青蛙从小到大落在石柱 L 上
  • t=1:1# 从 L 跳至荷叶 Y,L 上只剩 2# 3# 4#
  • t=2:2# 从 L 跳至石柱 S 的 S2 位置,L 上只剩 3# 和 4#
  • t=3:1# 从 Y 跳至 S,将 Y 清空。这时 S 上有 1#、2#,L 上有 3#、4#,好象原来在 L 上的 4 只青蛙分成了上下两部分,上面的 2 只通过荷叶 y 转移到了 S 上

这一过程是一分为二的过程:将 L 上的一队青蛙分解为两个队,每队各二只,并把上面的二只转移到 S 上。于是可形成两个系统:L、Y、R 系统与 S、Y、R 系统,前者二只青蛙号大,后者二只青蛙号小,先跳号大的、再跳号小的。从第五步到第九步可以看出的确是这么做的。

第36页:两个子系统求和

对于 LYR 系统,相当于 Jump(0,1);对于 SYR 系统,相当于 Jump(0,1)。两个系统之和为 2*Jump(0,1),因此:

Jump(1,1) = 2*Jump(0,1) = 2*2 = 4

现在再看 S=2、y=1 的 Jump(2,1):将河中两个石柱称作 S1 和 S2,荷叶叫 y,考虑先将 L 上青蛙的一半借助于 S2 和 y 转移到 S1 上,一半小号的青蛙在 S1 上、大的留在 L 上。

第37页:递归公式Jump(S,y)

这样 L S1 S2 y R 系统分解为:

(L S2 y R系统)+(S1 S2 y R系统) = 2 *(L S2 y R系统) = 2 * Jump(1,1)

用归纳法得:Jump(S, y) = 2*Jump(S-1, y)

第38页:青蛙过河的与或结点图

5. 将上述分析出来的规律写成递归形式的与或结点图。

第39页:计算举例Jump(3,4)

举例:S=3,y=4,算 Jump(3,4)。由 Jump(0,y)=y+1 与 Jump(S,y)=2*Jump(S-1,y) 可得 Jump(3,4)=2*Jump(2,4)=4*Jump(1,4)=8*Jump(0,4)=8*5=40,即最多可跳过 40 只青蛙。

说明:源文件该页仅给出“举例:S=3,y=4,算 Jump(3,4)”的标题性内容,上式为依据前两页递推关系补算的示例结果,供对照原图核对。

第40页:青蛙过河参考程序

#include <stdio.h> int Jump(int, int);void main() { int s,y,sum; printf("请输入石柱数s=" ); scanf("%d",&s); printf("请输入荷叶数y=" ); scanf("%d",&y); sum=Jump(s,y); printf("Jump(%d,%d)=%d\n",s,y,sum); } int Jump(int r,int z) { int k; if (r==0) { k=z+1; } else { k=2*Jump(r-1,z); } return(k); }

第41页:第六讲结束页

本页为第六讲 递归的结束页,本讲内容至此结束。

顶部目录