第4讲递归.docx
- 文档编号:27199909
- 上传时间:2023-06-28
- 格式:DOCX
- 页数:49
- 大小:73.02KB
第4讲递归.docx
《第4讲递归.docx》由会员分享,可在线阅读,更多相关《第4讲递归.docx(49页珍藏版)》请在冰豆网上搜索。
第4讲递归
第4讲递归
递归是算法设计中的一种基本而重要的算法。
递归方法即通过函数或过程调用自身将问题转化为本质相同但规模较小的子问题,是分治策略的具体体现。
递归方法具有易于描述、证明简单等优点,在动态规划、贪心算法、回溯法等诸多算法中都有着极为广泛的应用,是许多复杂算法的基础。
4.1递归概述
一个函数在它的函数体内调用它自身称为递归(recursion)调用。
是一个过程或函数在其定义或说明中直接或间接调用自身的一种方法,通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。
递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的能力在于用有限的语句来定义对象的无限集合。
用递归思想写出的程序往往十分简洁易懂。
一般来说,递归需要有边界条件、递归前进段和递归返回段。
当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
使用递归要注意以下几点:
(1)递归就是在过程或函数里调用自身;
(2)在使用递增归策略时,必须有一个明确的递归结束条件,称为递归出口。
例如有函数r如下:
intr(inta)
{b=r(a−1);
returnb;
}
这个函数是一个递归函数,但是运行该函数将无休止地调用其自身,这显然是不正确的。
为了防止递归调用无终止地进行,必须在函数内有终止递归调用的手段。
常用的办法是加条件判断,满足某种条件后就不再作递归调用,然后逐层返回。
构造递归方法的关键在于建立递归关系。
这里的递归关系可以是递归描述的,也可以是递推描述的。
例4-1用递归法计算n!
。
n!
的计算是一个典型的递归问题。
使用递归方法来描述程序,十分简单且易于理解。
(1)描述递归关系
递归关系是这样的一种关系。
设{U1,U2,U3,…,Un,…}是一个序列,如果从某一项k开始,Un和它之前的若干项之间存在一种只与n有关的关系,这便称为递归关系。
注意到,当n≥1时,n!
=n*(n−1)!
(n=0时,0!
=1),这就是一种递归关系。
对于特定的k!
,它只与k与(k−1)!
有关。
(2)确定递归边界
在步骤1的递归关系中,对大于k的Un的求解将最终归结为对Uk的求解。
这里的Uk称为递归边界(或递归出口)。
在本例中,递归边界为k=0,即0!
=1。
对于任意给定的N!
,程序将最终求解到0!
。
确定递归边界十分重要,如果没有确定递归边界,将导致程序无限递归而引起死循环。
例如以下程序:
#include
intf(intx)
{return(f(x−1));}
main()
{printf(f(5));}
它没有规定递归边界,运行时将无限循环,会导致错误。
(3)写出递归函数并译为代码
将步骤1和步骤2中的递归关系与边界统一起来用数学语言来表示,即
n!
=n*(n−1)!
当n>1时
n!
=1当n=1时
再将这种关系翻译为代码,即一个函数:
longf(intn)
{longg;
if(n<0)printf("n<0,输入错误!
");
elseif(n==1)g=1;
elseg=n*f(n−1);
return(g);
}
(4)完善程序
主要的递归函数已经完成,设计主程序调用递归函数即可。
#include
longf(intn)
{longg;
if(n<0)printf("n<0,输入错误!
");
elseif(n==1)g=1;
elseg=n*f(n-1);
return(g);
}
voidmain()
{intn;
longy;
printf("计算n!
,请输入n:
");scanf("%d",&n);
y=f(n);
printf("%d!
=%ld\n",n,y);
}
程序中给出的函数f是一个递归函数。
主函数调用f后即进入函数f执行,如果n<0,n==0或n=1时都将结束函数的执行,否则就递归调用f函数自身。
由于每次递归调用的实参为n−1,即把n−1的值赋予形参n,最后当n−1的值为1时再作递归调用,形参n的值也为1,将使递归终止,然后可逐层退回。
下面我们再举例说明该过程。
设执行本程序时输入为5,即求5!
。
在主函数中的调用语句即为y=f(5),进入f函数后,由于n=5,不等于0或1,故应执行f=f(n−1)*n,即f=f(5−1)*5。
该语句对f作递归调用即f(4)。
进行4次递归调用后,f函数形参取得的值变为1,故不再继续递归调用而开始逐层返回主调函数。
f
(1)的函数返回值为1,f
(2)的返回值为1*2=2,f(3)的返回值为2*3=6,f(4)的返回值为6*4=24,最后返回值f(5)为24*5=120。
综上,得出构造一个递归方法基本步骤,即描述递归关系、确定递归边界、写出递归函数并译为代码,最后将程序完善。
例4-2计算阿克曼函数
阿克曼(Ackerman)函数a(n,m)递归定义如下:
试输出阿克曼函数的(m≤3,n≤10)的值。
解:
a函数是一个随着变量m,n变化的双递归函数。
当m=0时,a(0,n)=n+1,这是递归终止条件;
当n=0时,a(m,0)=a(m-1,1);这是n=0时的递归表达式
当m,n≥1时,a(m,n)=a(m-1,a(m,n-1)),这是递归表达式。
试以a(1,3)为例说明函数的递归过程:
a(1,3)=a(0,a(1,2))=a(0,a(0,a(1,1)))=a(0,a(0,a(0,a(1,0))))
=a(0,a(0,a(0,a(0,1))))=a(0,a(0,a(0,2)))
=a(0,a(0,3))=a(0,4)=5
a函数及其调用描述如下:
inta(intm,intn)
{if(m==0)returnn+1;
elseif(n==0)returna(m-1,1);
elsereturna(m-1,a(m,n-1));
}
#include
voidmain()
{intm,n;
printf("a(m,n)");
for(n=0;n<=10;n++)
printf("n=%1d",n);
printf("\n");
for(m=0;m<=3;m++)
{printf("m=%d",m);
for(n=0;n<=10;n++)
printf("%5d",a(m,n));
printf("\n");
}
printf("\n");
}
程序运行求示例:
a(m,n)n=0n=1n=2n=3n=4n=5n=6n=7n=8n=9n=10
m=01234567891011
m=123456789101112
m=2357911131517192123
m=351329611252535091021204540938189
若采用递推求a(3,10),由上表可知a(3,9)=4093,则
a(3,10)=a(2,a(3,9))=a(2,4093)
n的取值是未知的且非常大,可见递推完成的难度。
4.2排队购票
1.问题提出
一场球赛开始前,售票工作正在紧张的进行中。
每张球票为50元,现有30个人排队等待购票,其中有20个人手持50元的钞票,另外10个人手持100元的钞票。
假设开始售票时售票处没有零钱,求出这30个人排队购票,使售票处不至出现找不开钱的局面的不同排队种数。
(约定:
拿同样面值钞票的人对换位置后为同一种排队。
)
2.递归设计要点
我们考虑一般情形:
有m+n个人排队等待购票,其中有m个人手持50元的钞票,另外n个人手持100元的钞票。
求出这m+n个人排队购票,使售票处不至出现找不开钱的局面的不同排队种数(这里正整数m,n从键盘输入)。
这是一道典型的组合计数问题,考虑用递推求解。
令f(m,n)表示有m个人手持50元的钞票,n个人手持100元的钞票时共有的方案总数。
我们分情况来讨论这个问题。
(1)n=0
n=0意味着排队购票的所有人手中拿的都是50元的钱币,注意到拿同样面值钞票的人对换位置后为同一种排队,那么这m个人的排队总数为1,即f(m,0)=1。
(2)m 当m (3)其它情况 我们思考m+n个人排队购票,第m+n个人站在第m+n-1个人的后面,则第m+n个人的排队方式可由下列两种情况获得: 1)第m+n个人手持100元的钞票,则在他之前的m+n-1个人中有m个人手持50元的钞票,有n-1个人手持100元的钞票,此种情况共有f(m,n-1)。 2)第m+n个人手持50元的钞票,则在他之前的m+n-1个人中有m-1个人手持50元的钞票,有n个人手持100元的钞票,此种情况共有f(m-1,n)。 由加法原理得到f(m,n)的递推关系: f(m,n)=f(m,n-1)+f(m-1,n) 初始条件: 当m 当n=0时,f(m,n)=1 3.购票排队递归程序实现 //购票排队 longf(intj,inti) {longy; if(i==0)y=1; elseif(j elsey=f(j-1,i)+f(j,i-1);//实施递归 return(y); } #include voidmain() {intm,n; printf("inputm,n: ");scanf("%d,%d",&m,&n); printf("f(%d,%d)=%ld.\n",m,n,f(m,n)); } 运行程序,inputm,n: 15,12,得 f(15,12)=4345965. 4.购票排队递推程序实现 //购票排队 #include voidmain() {intm,n,i,j; longf[100][100]; printf("inputm,n: ");scanf("%d,%d",&m,&n); for(j=1;j<=m;j++)//确定初始条件 f[j][0]=1; for(j=0;j<=m;j++) for(i=j+1;i<=n;i++) f[j][i]=0; for(i=1;i<=n;i++) for(j=i;j<=m;j++) f[j][i]=f[j-1][i]+f[j][i-1];//实施递推 printf("f(%d,%d)=%ld.\n",m,n,f[m][n]); } 运行程序,inputm,n: 20,10,得 f(20,10)=15737865. 比较以上两个程序,递推程序的运行速度要快于递归程序。 4.3汉诺塔问题 汉诺塔(Hanoi),又称河内塔问题,是印度的一个古老传说。 开天辟地的神勃拉玛在一个庙里留下了三根金刚石的棒,第一根上面套着64个圆的金片,最大的一个在底下,其余一个比一个小,依次叠上去。 庙里的众僧不倦地把它们一个个地从这根棒搬到另一根棒上,规定可利用中间的一根棒作为帮助,但每次只能搬一个,而且大的不能放在小的上面。 后来,这个传说就演变为汉诺塔游戏: 图35-1汉诺塔游戏示意图 (1)有三根桩子A、B、C。 A桩上有n个碟子,最大的一个在底下,其余一个比一个小,依次叠上去。 (2)每次移动一块碟子,小的只能叠在大的上面。 (3)把所有碟子从A桩全部移到C桩上,如图35-1所示。 试求解n个圆盘A桩全部移到C桩上的移动次数,并求出n个圆盘的移动过程。 4.3.1求移动次数 1.递归关系 当n=1时,只一个盘,移动一次即完成。 当n=2时,由于条件是一次只能移动一个盘,且不允许大盘放在小盘上面,首先把小盘从A桩移到B桩;然后把大盘从A桩移到C桩;最后把小盘从B桩移到C桩,移动3次完成。 设移动n个盘的汉诺塔需g(n)次完成。 分以下三个步骤: (1)首先将n个盘上面的n-1个盘子借助C桩从A桩移到B桩上,需g(n-1)次; (2)然后将A桩上第n个盘子移到C桩上(1次); (3)最后,将B桩上的n-1个盘子借助A桩移到C桩上,需g(n-1)次。 因而有递归关系: g(n)=2*g(n-1)+1 初始条件(递归出口): g (1)=1. 2.递归求解汉诺塔移动次数的程序设计 //汉诺塔n盘移动次数 #include voidmain() {doubleg(intm);//确定初始条件 intn; printf("请输入盘片数n: "); scanf("%d",&n); if(n<=40) printf("%d盘的移动次数为: %.0f\n",n,g(n)); else printf("%d盘的移动次数为: %.4e\n",n,g(n)); } //求移动次数的递归函数 doubleg(intm) {doubles; if(m==1) s=1; else s=2*p(m-1)+1; returns; } 3.程序运行示例 请输入盘片数n: 40 40盘的移动次数为: 1099511627775 请输入盘片数n: 64 64盘的移动次数为: 1.8447e+019 这是一个很大的天文数字,若每一秒移动一次,那么需要数亿个世纪才能完成这64个盘的移动。 4.3.2展示移动过程 1.求解思路 设递归函数hn(n,a,b,c)展示把n个盘从A桩借助B桩移到C桩的过程,函数mv(a,c)输出从a桩到c桩的过程。 完成hn(n,a,b,c),当n=1时,即mv(a,c)。 当n>1时,分以下三步: (1)将A桩上面的n-1个盘子借助C桩移到B桩上,即hn(n-1,a,c,b); (2)将A桩上第n个盘子移到C桩上,即mv(a,c); (3)将B桩上的n-1个盘子借助A桩移到C桩上,即hn(n-1,b,a,c)。 在主程序中,用hn(m,1,2,3)带实参m,1,2,3调用hn(n,a,b,c),这里m为具体移动盘子的个数。 同时设置变量k统计移动的次数。 2.展示汉诺塔移动过程的程序设计 函数mv(x,y)输出从x桩到y桩的过程,这里x,y分别不同情况取“A”或“B”或“C”,主函数调用hn(m,'A','B','C')。 //展示汉诺塔移动过程的递归设计 #include intk=0; voidmv(charx,chary)//输出函数 {printf("%c-->%c",x,y); k++;//统计移动次数 if(k%5==0) printf("\n"); } voidhn(intm,chara,charb,charc)//递归函数 {if(m==1)mv(a,c); else {hn(m-1,a,c,b); mv(a,c); hn(m-1,b,a,c); } } #include voidmain()//主函数 {intn; printf("\ninputn: ");scanf("%d",&n); hn(n,'A','B','C'); printf("\nk=%d\n",k);//输出移动次数 } 3.程序运行示例与分析 运行程序,inputn: 4 A-->BA-->CB-->CA-->BC-->A C-->BA-->BA-->CB-->CB-->A C-->AB-->CA-->BA-->CB-->C k=15 (1)上面的运行结果是实现函数hn(4,A,B,C)的过程,可分解为以下三步: 1)A-->BA-->CB-->CA-->BC-->AC-->BA-->B,这前7步是实施hn(3,A,C,B),即完成把上面3个盘从A桩借助C移到B桩。 2)A-->C这1步是实施着mv(A,C),即把最下面的盘从A桩移到C桩。 3)B-->CB-->AC-->AB-->CA-->BA-->CB-->C,这后7步是实施hn(3,B,A,C),即完成把B桩的3个盘借助A移到C桩。 (2)其中实现hn(3,A,C,B)的过程,可分解为以下三步: 1)A-->BA-->CB-->C,这前3步是实施hn(2,A,B,C),即完成把上面两个盘从A桩借助B移到C桩。 2)A-->B,这1步是实施mv(A,B),即把第3个盘从A桩移到B桩。 3)C-->AC-->BA-->B,这后3步是实施hn(2,C,A,B),即完成把C桩的两个盘借助A移到B桩。 从以上的结果分析可进一步帮助对递归的理解。 4.4旋转数阵 4.4.1双转向旋转方阵 1.案例提出 把前n2个正整数1,2,...,n2从左上角开始,由外层至中心按顺时针方向螺旋排列所成的数字矩阵,称n阶顺转方阵;按逆时针方向螺旋排列所成的称n阶逆转方阵。 12345116151413 161718196217242312 152425207318252211 142322218419202110 13121110956789 5阶顺转方阵5阶逆转方阵 下面即为一个5阶顺转方阵与5阶逆转方阵。 设计程序选择转向分别构造并打印这二种旋转方阵。 2.设计要点 设计以顺转展开,设置二维数组a[h][v]存放方阵中第h行第v列的整数。 把n阶方阵从外到内分圈,外圈内是一个n-2阶顺转方阵,只是起始数,具有与原问题相同的特性属性。 因此,设置旋转方阵递归函数t(a,b,s,d),其中a是存储方阵元素的数组;b为每个方阵的起始位置;d是为a数组赋值的整数;s是方阵的阶数。 b赋初值0,因数组下标从0开始,方阵的起始位置为(0,0)。 以后每一圈后进入下一内方阵,起始位置b需增1。 d从1开始递增1取值,分别赋值给数组的各元素,至n2 为止。 s从方阵的阶数n开始,以后每一圈后进入下一内方阵,s需减2。 s=0时返回,作为递归的出口; s=1时,即方阵只有一个数,显然为a[b][b]=d,返回。 s>1时,在函数t(a,b,s,d)中还需调用t(a,b+1,s-2,d)。 递归函数t(a,b,s,d)中对方阵的每一圈的各边的各个元素赋值: 1)一圈的上行从左至右递增 for(j=1;j {a[h][v]=d;v++;d++;}//行号h不变,列号v递增,数d递增 2)一圈的右列从上至下递增 for(j=1;j {a[h][v]=d;h++;d++;}//列号v不变,行号v递增,数d递增 3)一圈的下行从右至左递增 for(j=1;j {a[h][v]=d;v--;d++;}//行号h不变,列号v递减,数d递增 4)一圈的左行从下至上递增 for(j=1;j {a[h][v]=d;h--;d++;}//列号v不变,行号h递减,数d递增 经以上4步,完成一圈的赋值。 主程序中,只要带实参调用递归函数t(a,0,n,1)即可。 方阵按所选的转向以二维形式输出: p=1为顺转,输出a[h][v];p=2为逆转,输出a[v][h] 3.程序实现 //双转向旋转方阵递归设计 #include intn,a[20][20]={0}; voidmain() {inth,v,b,p,s,d; printf("请选择方阵阶数n: ");scanf("%d",&n); printf("请选择转向,顺转1,逆转2: ");scanf("%d",&p); b=1;s=n;d=1; voidt(inta[20][20],intb,ints,intd);//递归函数说明 t(a,b,s,d); if(p==1)//按要求输出旋转方阵 printf("%d阶顺转方阵: \n",n); else printf("%d阶逆转方阵: \n",n); for(h=1;h<=n;h++) {for(v=1;v<=n;v++) if(p==1) printf("%3d",a[h][v]); else printf("%3d",a[v][h]); printf("\n"); } return; } voidt(inta[20][20],intb,ints,intd)//定义递归函数 {intj,h=b,v=b; if(s==0)return;//递归出口 if(s==1) {a[b][b]=d;return;} for(j=1;j {a[h][v]=d;v++;d++;} for(j=1;j {a[h][v]=d;h++;d++;} for(j=1;j {a[h][v]=d;v--;d++;} for(j=1;j {a[h][v]=d;h--;d++;} t(a,b+1,s-2,d);//调用内一圈递归函数 } 4.程序运行示例与变通 请选择方阵阶数n: 7 请选择方向,顺转1,逆转2: 2 7阶逆转方阵: 1242322212019 2254039383718 3264148473617 4274249463516 5284344453415 6293031323314 78910111213 程序变通: 把方阵的输出元素作以下修改 a[h][v]修改为: n*n+1-a[h][v] a[v][h]修改为: n*n+1-a[v][h] 输出从中心开始旋转的数字方阵。 例如7阶逆转方阵,输出为: 49262728293031 48251011121332 47249231433 46238141534 45227651635 44212019181736 43424140393837 4.4.2顺转m×n矩阵 1.案例提出 当数阵的行数与列数不相等时,数阵称为矩阵。 显然,方阵是矩阵的特例。 如下为一5行6列的顺转矩阵。 123456 18192021227
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 第4讲 递归