栈与递归的实现.docx
《栈与递归的实现.docx》由会员分享,可在线阅读,更多相关《栈与递归的实现.docx(16页珍藏版)》请在冰豆网上搜索。
栈与递归的实现
论文题目:
栈与递归的关系
年级
专业信息与计算科学101
姓名
学号
日期:
2012.11.1
目录
1、内容提要-----------------------------------------------3
2、关键字--------------------------------------------------3
3、正文
第一部分:
递归的设计步骤---------------------------------4
第二部分:
举例说明-----------------------------------------4
(1)进制转换的栈与递归的实现------------------------5
(2)算阶乘的栈与递归的实现---------------------------7
(3)hanio塔的栈与递归的实现------------------------9
第三部分:
阐述递归与栈的关系(递归在计算机中的实现以阶乘为例)----------------------------------------------------------10
4、总结------------------------------------------------------12
5、参考文献------------------------------------------------13
栈与递归的关系
1、内容提要
在本篇论文中主要是讨论下栈与递归的关系,一个直接调用自己或者通过一系列的调用的语句间接地调用自己的函数,称作为递归函数。
调用函数与被调用函数直接的链接以及信息的交换就是通过栈来进行。
通常,当一个函数的运行期间调用另一个函数的时候,在运行被调用函数之前,系统都会先完成3件事:
(1)将所有的实在参数、返回地址等信息传递给被调用的函数保存;
(2)为被调用的局部变量分配存储区;(3)将控制转移到被调用的函数的入口。
而从被调用函数返回调用函数之前,系统也应完成三件事:
(1)保存被调用函数的计算结果;
(2)释放被调用函数的数据区;(3)依照被调用函数保存的返回地址将控制转移到调用函数上。
当有多个函数构成嵌套语句时,按照,“后调用先返回”的原则,上述的函数之间的信息传递和控制的转移必须通过“栈”来实现,即系统将整个程序运行时间所需的数据安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区,每当一个函数退出时,就释放它的存储空间,则当前正在运行的函数的数据区必在栈顶。
也即是说:
计算机执行递归算法时,是通过栈来实现的。
具体说来,就是在(递归过程或递归函数)开始运行时,系统首先为递归建立一个栈,该栈的元素类型(数据域)包括值参、局部变量和返回地址;在每次执行递归调用语句时之前,自动把本算法中所使用的值参和局部变量的当前值以及调用后的返回地址压栈(一般形象地称为“保存现场”,以便需要时“恢复现场”返回到某一状态),在每次递归调用结束后,又自动把栈顶元素(各个域)的值分别赋给相应的值参和局部变量(出栈),以便使它们恢复到调用前的值,接着无条件转向(返回)由返回地址所指定的位置继续执行算法。
所以实际上,在调用函数和被调用函数之间不仅会传递参数的值,也可以传递参数的地址。
在C语言中,对递归问题的编程时,不需要用户自己,而是由系统来管理递归工作栈。
2、关键词
栈与递归
3、正文
正文分成三个部分进行阐述:
第一部分:
递归的含义以及设计步骤和技巧
第二部分:
函数递归的应用例子
第三部分:
阐述递归与栈的关系(递归在计算机中的实现以阶乘为例)
第一部分:
递归的含义以及设计步骤和技巧
(1)递归的含义
所谓递归是指:
若在一个函数、过程或者数据结构定义的内部,直接(或间接)出现定义本身的应用,则称它们是递归的,或者是递归定义的。
递归是一种强有力的数学工具,它可使问题的描述和求解变得简洁和清晰。
递归算法常常比非递归算法更易设计,尤其是当问题本身或所涉及的数据结构是递归定义的时候,使用递归算法特别合适
(2)递归算法的设计步骤
第一步骤(递归步骤):
将规模较大的原问题分解为一个或多个规模更小、但具有类似于原问题特性的子问题。
即较大的问题递归地用较小的子问题来描述,解原问题的方法同样可用来解这些子问题。
第二步骤:
确定一个或多个无须分解、可直接求解的最小子问题(称为递归的终止条件)。
(3)在递归算法的内部实现中所起的作用。
①调用函数时:
系统将会为调用者构造一个由参数表和返回地址组成的活动记录,并将其压入到由系统提供的运行时刻栈的栈顶,然后将程序的控制权转移到被调函数。
若被调函数有局部变量,则在运行时刻栈的栈顶也要为其分配相应的空间。
因此,活动记录和这些局部变量形成了一个可供被调函数使用的活动结构。
注意:
参数表的内容为实参,返回地址是函数调用语句的下一指令的位置。
②被调函数执行完毕时:
系统将运行时刻栈栈顶的活动结构退栈,并根据退栈的活动结构中所保存的返回地址将程序的控制权转移给调用者继续执行。
第二部分:
函数递归的应用例子
函数的递归在现实生活中非常常见,比如我们所熟悉的hanoi塔问题,以及我们学数学经常用到的阶乘问题,像这些看上去非常复杂的问题,往往用递归函数来实现的话,及其的语言是非常精炼的。
在本篇小论文中我将例举几个简单的例子,分别采用递归的方法以及平常普通的编程。
来进一步阐述递归函数的精妙之处。
(1)首先我们来编写一个程序,用于进制转换的问题,比如任意输入一个十进制数,使其输出相对应的八进制数。
在设计这个算法的时候我们会发现,十进制数转换为八进制数会进行很多的求余数的操作,其实这个过程就是一个重复的执行过程。
源程序如下:
(采用了递归的算法)
#defineTRUE1
#defineFALSE0
#defineOK1
#defineERROR0
#defineINFEASIBLE-1
#defineOVERFLOW-1
#defineSTACK_INIT_SIZE100
//储存空间的初始分配量
#defineSTACKINCREMENT10//存储空间的分配增量
typedefintStatus;
typedefintSElemType;//元素为整型
#include"stdio.h"
#include"stdlib.h"
#include"math.h"
typedefstruct
{
SElemType*base;
SElemType*top;
intstacksize;//当前已经分配的存储空间,以元素为单位
}SqStack;//顺序栈
StatusInitStack(SqStack&S)//构造一个空的顺序栈
{
S.base=(SElemType*)malloc(STACK_INIT_SIZE*sizeof(SElemType));
if(!
S.base)exit(OVERFLOW);
S.top=S.base;
S.stacksize=STACK_INIT_SIZE;
returnOK;
}//Initstack*/
Statuspush(SqStack&S,SElemTypee)//插入元素为e为新的栈顶元素
{
if(S.top-S.base>=S.stacksize)
{
S.base=(SElemType*)realloc(S.base,(S.stacksize+STACKINCREMENT)*sizeof(SElemType));
if(!
S.base)exit(OVERFLOW);
S.top=S.base+S.stacksize;
S.stacksize+=STACKINCREMENT;
}
*S.top++=e;
returnOK;
}
Statuspop(SqStack&S,SElemType&e)
{
if(S.top==S.base)returnERROR;
e=*--S.top;
returnOK;
}
StatusStackEmpty(SqStackS)
{
return(S.top==S.base);
}
voidzhuanghuan(int&N)//递归函数
{SqStack(S);
InitStack(S);
SElemTypee;
if(N!
=0)
{
push(S,N%8);
N=N/8;
}
while(N)
{
zhuanghuan(N);
}
while(!
StackEmpty(S))
{
pop(S,e);
printf("%d",e);
}
}
main()
{
intN;
printf("请输入一个十进制数N\n");
scanf("%d",&N);
zhuanghuan(N);
printf("此为对应的八进制转化");
printf("\n");
}注意:
在上述的进制转换中其递归函数并不是很明显,但是我们进过下一步的分析,就可以发现:
voidzhuanghuan(int&N)
{SqStack(S);
InitStack(S);
SElemTypee;
if(N!
=0)
{
push(S,N%8);
N=N/8;
}
while(N)
{
zhuanghuan(N);
}
while(!
StackEmpty(S))
{
pop(S,e);
printf("%d",e);
}
}
在执行zhuanghuan这个函数的时候zhuanghuan函数又调用了本身,这就是递归。
注意:
这个程序主要是想要阐述下递归与栈的关系,上面说,系统在执行递归函数时系统会自动的分配空间,这里是自己建立一个栈,然后把运算的数据放在里面。
(2)用递归算法求N阶乘(N!
=1*2*3*……*N,N<20);
源程序如下:
#include"stdio.h"
main()
{floatfac(intn);
intn;
floaty;
printf("请输入要求的数据的阶乘\n");
scanf("%d",&n);
y=fac(n);
printf("********************************\n");
printf("%d!
=%.0f\n",n,y);
printf("********************************\n");
}
floatfac(intn)
{
floatf;
if(n<0)
printf("error\n");
elseif(n==1||n==0)
f=1;
else
f=n*fac(n-1);
returnf;
}
分析计算过程:
我们假设在主函数里面输入的数据为3,即y=fac(3);那么fac的函数执行如下:
(1)当n=3时进入函数,执行f=3*fac
(2);产生一个f值,但是并不是就把f值传递出去。
此时程序就会进入fac
(2),传入的参数n就为2了,但是并不传出return(f),return(f)要等fac
(2)执行完成后才能执行;
(2)fac
(2)中f=2*fac(2-1),此时程序就会进入fac
(1),传入的参数n就变为1;但是并不传出return(f),return(f)要等fac
(1)执行完成后才能执行;
(3)fac
(1)中符合if(n==1||n==0),所以f=1;此时fac
(1)就执行完毕,现在fac
(1)执行完毕后产生一个结果f=1,就将结果return给fac
(2),在fac
(2)中的f=2*1=2,执行好fac
(2)后产生的f=2,return给fac(3),fac(3)=3*fac
(2)=3*2=6;
这样才使得y=fac(6)=6;
(3)用递归与栈来表示hanio塔问题
#include"stdio.h"
voidhanoi(intn,chara,charb,charc)
{
if(n==1)
printf("将%d个盘直接从%c移到%c\n",n,a,c);
else
{
hanoi(n-1,a,c,b);
printf("将第%d个盘从%c移动到%c\n",n,a,c);
hanoi(n-1,b,a,c);
}
}
voidmain()
{
intn;
printf("请输入需要移动的盘子的个数\n");
scanf("%d",&n);
printf("***********************************\n");
hanoi(n,'x','y','z');
printf("***********************************\n");
}
2,a,c,b
3,a,b,c
分析:
这是一个非常典型的栈与递归的结合使用,我们就从简单的三个盘子进行入手。
这个递归的边界条件是if(n==1),当我们输入为3时,系统就会执行hanio(3,a,b,c),然后把
入栈,接着执行hanio(2,a,c,b),然后把入栈,再执行hanio(1,a,b,c),按照这个秩序进行。
先用流程图表示系统栈的插入和输出:
Hanio塔中栈与递归的流程图
第三部分:
阐述递归与栈的关系(递归在计算机中的实现,以阶乘为例)
在第二部分中我们分析了递归函数,在算n的阶乘里面我们逐步的分析了函数调用的过程,那么,递归函数与栈到底有什么关系呢?
在算阶乘的时候,n=3的时候,即y=fac(3),判断可知执行得到f=3*fac
(2);但是,这并不是确切的值,还有fac的函数调用,所以系统就会分配一个顺序栈来存放当前计算出的结果,相当于在执行完fac(3)后在执行fac
(2)之前,系统就会在栈顶插入f=3*fac
(2)以及一个returnf的返回地址。
把这些运算得到的东西压入栈顶后,然后去执行fac
(2),)产生f=2*fac
(1),n变为了1,此时还没有结束,在执行fac
(1)前,系统又把产生的f=2*fac
(1),一个returnf的返回地址又压入到先前开辟的栈中,然后去执行fac
(1),执行fac
(1)时,符合if(n==1||n==0),所以f=1,此时f=1;f变为了常数,此时系统就会把f=1,,一个returnf的返回地址,输入到栈中。
然后就不在进行向后的fac函数运算,此时递归调用就已经结束了,下个阶段就是数据出栈了,系统会自动的把栈顶的数据读出来,即首先把f=1和一个returnf的返回地址读出,然后读出第二个数据f=2*fac
(1)以及一个return的返回地址,但是第一个数据中的returnf的返回地址与第二个是不同的,当读出第一个returnf的返回值后,系统就会把第一个数据中的f=1,返回到第二个数据中的f=2*fac
(1),此时返回值f=1就赋给fac
(1),即fac
(1)=f=1;,此时f=2*fac
(1)=2;;然后按照读出的第二个数据中的返回地址,将f=2返回到第三个数据中的fac
(2)中,即:
f=3*fac
(2)=3*f=3*2=6;第三个数据执行完后f=6;按照第三个数据的return返回值,就把f赋值给了主程序中的y.以上便是在执行递归函数时,数据的入栈和出栈过程。
我们可以清楚的发现,递归函数刚好利用了栈的后进先去的特性。
下面将用流程图表示出来递归的与栈的关系。
从栈中读取数据以及returnf的返回值,以及运算过程
四、总结
在正文里面我们分析了三个例子,第一个是十进制转换成八进制的问题,这个程序是采用自己建立一个栈来存放计算过程中的数据,感觉上比较复杂,但是,其函数确实用到了递归函数。
只是我们把它给形象化了。
我们可以清楚的看到数据入栈和出栈的过程以及分析结果。
第二个例子是,阶乘运算。
而这个程序相比较而言就比较简单方便,就几行就把问题解决了。
代码更简洁清晰,可读性更好。
但是数据是咋样存放的以及存放什么数据,我们都不知道。
如果把第一个例子与第二个例子结合起来看就能发现栈与递归的实现。
第三个例子就是hanio问题,这个例子当中有不断的入栈和出栈的过程,总体来说分析是有点困难,但是能发现用的非常巧妙。
生活中,汉罗塔问题就用到递归的思想,还有迷宫问题,所以,使用递归函数是一种非常方便的数学工具。
但是,递归也是有一个重要的缺点。
因为我们在使用递归函数时,在还没有结束递归过程时,每个计算过程所产生的数据以及变量的值和返回地址都将存储起来,也称作“保护现场”,是为了“还原现场”。
但是,如果当这个函数所进行的次数非常多的时候。
此时,系统就会产生非常多的临时数据,而系统都会把这些数据存放起来。
所以,造成了非常占用内存,那个系统分配的栈的容量会非常大,导致了数据冗余,数据达到一定的数据量时,可能导致电脑会死机。
所以,递归函数好用,我们也要小心的用。
普通的小程序,应该是没有什么问题的。
这样就会导致这个程序运行时间比较长,达不到预期的效果。
在递归过程中,其实我们并不用去了解计算机内部的操作,因为这些计算机系统本身就会自己建立一个栈来存放数据,等到递归结束后系统会自动的开始读取数据和返回值。
等到递归全部结束后,系统就会释放存放临时数据的栈的空间。
4、参考文献
1、数据结构(c语言版)严蔚敏
2、C程序设计教程谭浩强
3、数据结构教程(第二版)李春葆
2012.11.1