目录
3.5 队列的类型定义
3.5.2 循环队列——队列的顺序表示和实现
1.初始化
2.求队列长度
3.入队
4.出队
5.取队头元素
3.5.3 链队——队列的链式表示和表现
1.初始化
2.入队
3.出队
4.取队头元素
3.6 案例分析与实现
案例3.1:数制的转换
案例3.2:括号匹配的检验
案例3.3:表达式求值
案例3.4:舞伴问题
3.7小结
队列的操作与栈的操作类似,不同的是,删除是在表的头部(队头)进行。
下面给出队列的抽象数据类型定义。
ADT Queue {
数据对象:D={}
数据操作:R={} 约定其中端为队列头,端为队列尾。
基本操作:
InitQueue(&Q)
操作结果:构造一个空队列Q。
DestroyQueue(&Q)
初始条件:队列Q已存在。
操作结果:队列Q被销毁,不再存在。
ClearQueue(&Q)
初始条件:队列Q已存在。
操作结果:将Q清为空队列。
QueueEmpty(Q)
初始条件:队列Q已存在。
操作结果:若Q为空队列,则返回true,否则返回false。
QueueLength(Q)
初始条件:队列Q已存在。
操作结果:返回Q的元素个数,即队列的长度。
GetHead(Q)
初始条件:Q为非空队列。
操作结果:返回Q的队头元素。
EnQueue(&Q,e)
初始条件:队列Q已存在。
操作结果:插入元素e为Q的新的队尾元素。
DeQueue(&Q,&e)
初始条件:Q为非空队列。
操作结果:删除Q的队头元素,并用e返回其值。
QueueTraverse(Q)
初始条件:Q已存在且非空。
操作结果:从队头到队尾,依次对Q的每个数据元素访问。
}ADT Queue
和栈类似,在本书后面内容中引用的队列都是如上定义的队列类型,队列的数据元素类型在应用程序内定义。
队列也有两种存储表示,顺序表示和链式表式。
和顺序栈相类似,在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队列头到队列尾的元素之外,尚需附设两个整型变量front和rear分别指示队列头元素和队列尾元素的位置(后面分别称为头指针和尾指针)。队列的顺序存储结构表示如下:
#define MAXQSIZE 100
typedef struct
{
QElemType *base; //存储空间的基地址
int front; //头指针
int rear; //尾指针
}SqQueue;
为了在C语言中描述方便起见,在此约定:初始化创建空队列时,令front=rear=0,每当插入新的队列尾元素时,尾指针rear增1;每当删除队列头元素时,头指针front增1。因此,在非空队列中,头指针始终指向队列头元素,而尾指针始终指向队列尾元素的下一个位置,如下图所示。
假设当前队列分配的最大空间为6,则当队列处于上图(d)所示的状态时不可再继续插入新的队尾元素,否则会出现溢出现象,即因数组越界而导致程序的非法操作错误。事实上,此时队列 实际可用空间并未占满,所以这种现象称为“假溢出”。这是由“队尾入队,队头出队”这种受限制的操作造成的。
怎样解决这种“假溢出”问题呢?一个较巧妙的办法是将顺序队列变为一个环状空间,如下图所示,称之为循环队列。
头、尾指针以及队列元素之间的关系不变,只是在循环队列中,头、尾指针“依环状增1”的操作可用“模”运算来实现。通过取模,头指针和尾指针就可以在顺序表空间内以头尾衔接的方式“循环”移动。
在下图(a)中,队头元素是,在元素入队之前,在Q.rear的值为5,当元素入队之后,通过模运,Q.rear=(Q.rear+1)%6,得到Q.rear的值为0,而不会出现下图(d)的“假溢出”状态。
在上图(b)中,相继入队,则队列空间均被占满,此时头、尾指针相同。
在上图(c)中,若和相继从(a)所示的队列中出队,使队列此时呈“空”的状态,头、尾指针的值也是相同的。
由此可见,对于循环队列不能以头、尾指针的值是否相同来判别队列空间是“满”还是“空”。在这种情况下,如何区别队满还是队空呢?
通常有以下两种处理方法。
1)少用一个元素空间,即队列空间大小为m时,有m-1个元素就认为是队满。这样判断队空的条件不变,即当头、尾指针相同时,则认为队空;而当尾指针在循环意义上加1后是等于头指针,则认为队满。因此在循环队列中队空和队满的条件是:
队空的条件:Q.front=Q.rear
队满的条件:(Q.rear+1)%MAXQSIZE=Q.front
如上图(d)所示,当J7、J8、J9进入上图(a)所示的队列后,(Q.rear+1)%MAXQSIZE的值等于Q.front,此时认为队满。
2)另设一个标志位以区别队列是“空”还是“满”。
下面给出第一种方法实现循环队列的操作,循环队列的类型定义同前面给出的顺序队列的类型定义。
循环队列的初始化操作就是动态分配一个预定义大小为MAXQSIZE的数组空间。
算法3.11 循环队列的初始化
【算法步骤】
1)为队列分配一个最大容量为MAXQSIZE的数组空间,base指向数组空间的首地址。
2)头指针和尾指针置为零,表示队列为空。
【算法描述】
Status IniQueue(SqQueue &Q)
{//构造一个空队列
Q.base=new QElemType[MAXQSIZE];
if(!Q.base)
exit(OVERFLOW);
Q.front=Q.rear=0;
return OK;
}
对于非循环队列,尾指针和头指针的差值便是队列长度,而对于循环队列,差值可能为负数。所以需要将差值加上MAXQSIZE。然后与MAXQSIZE求余。
算法3.12 求循环队列的长度
【算法描述】
int QueueLength(SqQueue Q)
{//返回Q的元素个数,即队列的长度
rerturn(Q.rear-Q.front+MAXQSIZE)&MAXQSIZE;
}
入队操作是指在队尾插入一个新的元素。
算法3.13 循环队列的入队
【算法步骤】
1)判断队列是否满,若满则返回ERROR。
2)将新元素插入队尾。
3)队尾指针加1。
【算法描述】
Status EnQueue(SqQueue &Q,QElemType e)
{//插入元素e为Q的新的队尾元素
if((Q.rear+1)%MAXQSIZE==Q.front)
return ERROR;
Q.base[Q.rear]=e;
Q.rear=(Q.rear+1)%MAXQSIZE;
return ok;
}
出队操作是将队头元素删除。
算法3.14 循环队列的出队
【算法步骤】
1)判断队列是否为空,若空则返回ERROR。
2)保存队头元素。
3)队头指针加1。
【算法描述】
Status DeQueue(SqQueue &Q,QElemType &e)
{//删除Q的队头元素,用e返回其值
if(Q.front==Q.rear)
return ERROR;
e=Q.base[Q.front];
Q.front=(Q.front+1)%MAXQSIZE;
return OK;
}
当队列为非空时,此操作返回当前队头元素的值,队头指针保持不变。
算法3.15 取循环队列的队头元素
【算法描述】
QElemType GetHead(SqQueue Q)
{//返回Q的队头元素,不修改队头指针
if(Q.front!=Q.rear)
return Q.base[Q.front];
}
由上述分析可见,如果用户的应用程序中设有循环队列,则必须为它设定一个最大队列长度;若用户无法预估所用队列的最大长度,则宜采用链队。
链队是指采用链式存储结构实现的队列。通常链队用单链表来表示,如下图所示
一个链队显然需要两个分别指示队头和队尾的指针(分别称为头指针和尾指针)才能唯一确定。这里和线性表的单链表一样,为了操作方便起见,给链队添加一个头结点,并令头指针始终指向头结点。队列的链式存储结构表示如下:
typedef struct QNode
{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct
{
QueuePtr front;
QueuePtr rear;
}LinkQueue;
链队的操作即为单链表插入和删除操作的特殊情况,只是需进一步修改尾指针或头指针。下面给出链队初始化、入队、出队操作的实现。
链队的初始化就是构造一个只有一个头结点的空对,如下图所示。
算法3.16
【算法步骤】
1)生成新结点作为头结点,队头和队尾指针指向此结点。
2)头结点的指针域置空。
【算法描述】
Status InitQueue(LinkQueue &Q)
{//构造一个空队列Q
Q.front=Q.rear=new QNode;
Q.front->next=NULL;
return OK;
}
和循环队列的入队操作不同的是,链队在入队前不需要判断队是否为满,需要为入队元素动态分配一个结点空间,如上图(b)和(c)所示。
算法3.17 链队的入队
【算法步骤】
1)为入队元素分配结点空间,用指针p指向。
2)将新结点数据域置为e。
3)将新结点插入到队尾。
4)修改队尾指针为p。
【算法描述】
Status EnQueue(LinkQueue &Q,QElemType e)
{//插入元素e为Q的新的队尾元素
p=new QNode;
p->data=e;
p->next=NULL;Q.rear->next=p;
Q.rear=p;
return OK;
}
和循环队列一样,链队在出队前也需要判断队列是否为空,不同的是,链队在出队后需要释放出队头元素的所占空间,如上图(d)所示。
算法3.18 链队的出队
【算法步骤】
1)判断队列是否为空,若空则返回ERROR。
2)临时保存队头元素的空间,以备释放。
3)修改头结点的指针域,指向下一个结点。
4)判断出队元素是否为最后一个元素,若是,则将队尾指针重新赋值,指向头结点。
5)释放原队头元素的空间。
【算法描述】
Status DeQueue(LinkQueue &Q,QElemType &e)
{//删除Q的队头元素,用e返回其值
if(Q.front==Q.rear)
return ERROR;
p=Q.front->next;
e=p->data;
Q.front->next=p->next;
if(Q.rear==p)
Q.rear=Q.front;
delete p;
return OK;
}
需要注意的是,在链队出队操作时还要考虑当队列中最后一个元素被删除后,队列尾指针也丢失了,因此需对队尾指针重新赋值(指向头结点)。
与循环队列一样,当队列非空时,此操作返回当前队头元素的值,队头指针保持不变。
算法3.19 取链队的队头元素
【算法描述】
QElemType GetHead(LinkQueue Q)
{//返回Q的队头元素,不修改队头指针
if(Q.front!=Q.rear)
return Q.front->next->data;
}
在3.2节中我们引入了3个有关栈应用的案例和一个有关队列应用的案例。本节队这四个案例作进一步的分析,然后分别利用栈和队列的基本操作给出案例中相关算法的具体实现。
【案例分析】
当将要给十进制整数N转换为八进制数时,在计算过程中,把N与8求余得到的八进制数的各位依次进栈,计算完毕后将栈中的八进制数依次出栈输出,输出结果就是待求得的八进制数。
【案例实现】
在具体实现时,栈可以采用顺序存储表示也可以采用链式存储表示。
算法3.20 数制的转换
【算法步骤】
1)初始化一个空栈S。
2)当十进制数N非零时,循环执行以下操作:
把N与8求余得到的八进制数压入栈S;
N更新为N与8的商。
3)当栈S非空时,循环执行以下操作:
弹出栈顶元素e;
输出e。
【算法描述】
void conversion(int N)
{//对于任意一个非负十进制数,打印输出与其等值的八进制数
InitStack(S);
while(N)
{
Push(S,N%8); //把N与8求余得到的八进制数压入栈S
N=N/8; //N更新为N与8的商
}
while(!StackEmpty(S))
{
Pop(S,e); //弹出栈顶元素e
count<
【算法分析】
显然,该算法的时间和空间复杂度均为。
这是利用栈的后进先特性的最简单的例子。在这个例子中,栈的操作是单调的,即先一味地入栈,然后一味地出栈。也许,有的读者会提出问题:用数组直接实现不是更简单吗?但仔细分析上述算法不难看出,栈的引入简化了程序设计的问题,划分了不同的关注层次,使思考范围缩小了。而用数组不仅掩盖了问题的本质,还要分散精力取考虑数组下标递增减等细节问题。
在实际利用栈的问题中,入栈和出栈操作大都不是单调的,而是交错进行的。下面的案例3.2和3.3都属于这种情况。
【案例分析】
检验算法借助一个栈,每当读入一个左括号,则直接入栈,等待相匹配的同类右括号;每当读入一个右括号,若与当前栈顶的左括号类型相同,则二者匹配,将栈顶的左括号入栈,直到表达式扫描完毕。
在处理过程中,还要考虑括号不匹配出错的情况。例如,当出现(()[]))这种情况时,由于前面入栈的左括号均已和后面出现的右括号相匹配,栈已空,因此最好扫描的右括号不能得到匹配;出现[([])这种错误,当表达式扫描结束时,栈中还有一个左括号没有匹配;出现(()]这种错误显然是栈顶的左括号和最后的右括号不匹配。
【案例实现】
算法3.21 括号的匹配
【算法步骤】
1)初始化一个空栈S。
2)设置一标记行变量flag,用来标记匹配结果以控制循环及返回结果,1表示正确匹配,0表示错误匹配,flag初值为1。
3)扫描表达式,依次读入字符ch,如果表达式没有扫描完毕且flag非零,则循环执行以下操作:
若ch是左括号“[”或“(”,则将其压入栈;
若ch是右括号“)”,则根据当前栈顶元素的值分情况考虑:若栈非空且栈顶元素是“(”,则正确匹配,否则错误匹配,flag置为0;
若ch是右括号“]”,则根据当前栈顶元素的值分情况考虑:若栈非空且栈顶元素是“[”,则正确匹配,否则错误匹配,flag置为0。
4)退出循环后,如果栈空且flag值为1,则匹配成功,返回ture,否则返回false。
【算法描述】
Status Matching()
{//表达式以“#”结束
InitStack(S);
flag=1;
cin>>ch;
while(ch!='#'&&flag)
{
switch(ch)
{
case'[':
case'(':
Push(S,ch);
break;
case')':
if(!StackEmpty(S)&&GetTop(S)=='(')
Pop(S,x);
else
flag=0;
case']':
if(!StackEmpty(S)&&GetTop(S)=='[')
Pop(S,x);
else
flag=0;
break;
}
cin>>ch;
}
if(StackEmpty(S)&&flag)
return ture;
else
return false;
}
【算法分析】
此算法从头到尾扫描表达式中每个字符,若表达式的字符串长度为n,则此算法的时间复杂度为O(n)。算法在运行时所占用的辅助空间主要取决于S栈的大小,显然,S栈的空间大小不会超过n,所以此算法的空间复杂度也同样为O(n)。
【案例分析】
任何一个表达式都是由操作数(operand)、运算符(operator)和界限符(delimiter)组成的,统称它们为单词。一般地,操作数既可以是常数,也可以是被说明为变量或常量的标识符;运算符可以分为算术运算符、关系运算符和逻辑运算符3类;基本界限符有左右括号和表达式结束符等。为了叙述的简洁,在此仅讨论简单算术表达式的求值问题,这种表达式只含加、减、乘、除4种运算符。读者不难将它推广到更一般的表达式上。
下面把运算符和界限符统称为算符。
我们知道,算术四则运算遵循以下3条规则:
1)先乘除后加减;
2)从左算到右;
3)先括号内,后括号外。
根据上述3条运算规则,在运算的每一步种,任意两个相继出现的算符和之间的优先关系,至多是下面3种关系之一:
<,的优先权低于
=,的优先权等于
>,的优先权高于
下表定义了算符之间的这种优先关系
+ | - | * | / | ( | ) | # | |
+ | > | > | < | < | < | > | > |
- | > | > | < | < | < | > | > |
* | > | > | > | > | < | > | > |
/ | > | > | > | > | < | > | > |
( | < | < | < | < | < | = | |
) | > | > | > | > | > | > | |
# | < | < | < | < | < | = |
由规则(1),先进行乘除运算,后进行加减运算,所以有“+”<“*”;“+”<“/”;“*">“+”;“/”>“+”等。
由规则(2),运算遵循左结合性,当两个运算符相同时,先出现的运算符优先级高,所以有“+”>“+”;“-”>“-”;“*”>“*”;“/”>“/”。
由规则(3),括号内的优先级高,+、-、*和/为时的优先性均低于“(”但高于“)”。
表中的“(”=“)”表示当左右括号相遇时,括号内的运算已经完成。为了便于实现,假设每个表达式均以“#”开始,以“#”结束。所以“#”=“#”表示整个表达式求值完毕。“)”与“(”、“#”与“)”以及“(”与“#”之间无优先关系,这是因为表达式种不允许他们相继出现。一旦遇到这种情况,则可以认为出现了语法错误。在下面的讨论中,我们暂假定所输入的表达式不会出现语法错误。
【案例实现】
为实现算符优先算法,可以使用两个工作栈,一个称作OPTR,用以寄存运算符;另一个称作OPND,用以寄存操作数或运算结果。
算法3.22 表达式求值
【算法步骤】
1)初始化OPTR栈和OPND栈,将表达式起始符“#”压入OPTR栈。
2)扫描表达式,读入第一个字符ch,如果表达式没有扫描完毕至“#”或OPTR的栈顶元素不为“#”时,则循环执行以下操作:
若ch不是运算符,则压入OPND栈,读入下一个字符ch;
若ch是运算符,则根据OPTR的栈顶元素和ch的优先级比较结果,做不同的处理:
若是小于,则ch压入OPTR栈,读入下一字符ch;
若是大于,则弹出OPTR栈顶的运算符,从OPND栈弹出两个数,进行相应运算,结果压入OPND栈;
若是等于,则OPTR的栈顶元素是“(”且ch是“)”,这时弹出OPTR栈顶的“(”,相当于括号匹配成功,然后读入下一字符ch。
3)OPND栈顶元素即为表达式求值结果,返回此元素。
【算法描述】
char EvaluateExpression()
{
InitStack(OPND);
InitStack(OPTR);
Push(OPTR,'#'); //将表达式起始符“#”压入OPTR栈
cin>>ch;
while(ch!='#'||GetTop(OPTR)!='#')
{
if(!In(ch))
{
Push(OPND,ch);
cin>>ch;
}
else
switch(Precede(GetTop(OPTR),ch))
{
case'<':
Push(OPTR,ch);cin>>ch;
break;
case'>':
Pop(OPTR,theta);
Pop(OPND,b);Pop(OPND,a);
Push(OPND,Operate(a,theta,b));
break;
case'=':
Pop(OPTR,x);cin>>ch;
break;
}
}
return GetTop(OPND);
}
算法调用的三个函数需要读者自行补充完成。其中函数In是判定读入的字符ch是否为运算符,Precede是判定运算符栈的栈顶元素与读入的运算符之间优先关系的函数,Operate为进行二元运算的函数。
另外需要特别说明的是,上述算法中的操作数只能是一位数,因为这里使用的OPND栈是字符站,如果需要进行多位数的运算,则需要将OPND栈改为数栈,读入的数字字符拼成数之后再入栈。读者可以改进此算法,使之能完成多位数的运算。
【算法分析】
同算符3.21一样,此算法从头到尾扫描表达式中每个字符,若表达式中的字符串长度为n,则此算法的时间复杂度为O(n)。算法在运行时所占用的辅助空间主要取决于OPTR栈和OPND栈的大小,显然,它们的空间大小之和不会超过n,所以此算法的空间复杂度也同样为O(n)。
【例3.2】 算法表达式的求值过程
利用算法3.22对算术表达式3*(7-2)进行求值,给出其求值的具体过程。
在表达式两端先增加“#”,改写为 #3*(7-2)#
具体操作过程如下表所示
步骤 | OPTR栈 | OPND栈 | 读入字符 | 主要操作 |
1 | # | 3 | Push(OPND,'3') | |
2 | # | 3 | * | Push(OPTR,'*’) |
3 | #* | 3 | ( | Push(OPTR,'(') |
4 | #*( | 3 | 7 | Push(OPND,'7') |
5 | #*( | 3 7 | - | Push(OPTR,'-') |
6 | #*(- | 3 7 | 2 | Push(OPND,'2') |
7 | #*(- | 3 7 2 | ) | Push(OPND,Operate('7','-','2')) |
8 | #*( | 3 5 | ) | Push(OPTR){消去一对括号} |
9 | #* | 3 5 | # | Push(OPND,Operate('3','*','5')) |
10 | # | 15 | # | return(GetTop(OPND)) |
在高级语言的编译处理过程中,实际上不只是表达式求值可以借助栈来实现,高级语言中一般语法成分的分析都可以借助栈来实现,在编译原理后续课程中会涉及栈在语法、语义等分析算法中的应用。
【案例分析】
对于舞伴配对问题,先入队的男士或女士先出队配成舞伴,因此设置两个队列分别存放男士和女士入队者。假设男士和女士的记录存放在一个数组中作为输入,然后依次扫描该数组的各元素,并根据性别来决定是进入男队还是女队。当这两个队列构造完成之后,依次将两队当前的队头元素出队来配成舞伴,直至队列变空为止。此时,若某队仍有等待配对者,则输出此队列中排在队头的等待者的姓名,此人将是下一轮舞曲开始时第一个可获得舞伴的人。
【案例实现】
typedef struct
{
char name[20];
char sex;
}person;
#define MAXQSIZE 100
typedef struct
{
Person *base;
int front;
int rear;
}SqQueue;
SqQueue Mdancers,Fdancers;
算法3.23 舞伴问题
【算法步骤】
1)初始化Mdancers队列和Fdancers队列。
2)反复循环,依次将跳舞者根据其性别插入Mdancers队列或Fdancers队列。
3)当Mdancers队列和Fdancers队列均为非空时,反复循环,依次输出男女舞伴的姓名。
4)如果Mdancers队列为空而Fdancers队列非空,则输出Fdancers队列的队头女士的姓名。
5)如果Fdancers队列为空而Mdancers队列非空,则输出Mdancers队列的队头男士的姓名。
【算法描述】
void DancePartner(Person dancer[],int num)
{//结构数组dancer中存放跳舞的男女,num是跳舞的人数。
InitQueue(Mdancers);
InitQueue(Fdancers);
for(i=0;i
【算法分析】
若跳舞者人数总计为n,则此算法的时间复杂度O(n)。空间复杂度取决于Mdancers队列和Fdancers队列的长度,二者长度之和不会超过n,因此空间复杂度也同样为O(n)。
队列在程序涉及中也有很多应用,凡是符合先进先出原则的数学模型,都可以用队列。最典型的例子是操作系统中用来解决主机与外设之间速度不匹配问题或多个用户引起的资源竞争问题。
例如,一个局域网上有一台共享的网络打印机,网络上每个用户都可以将数据发送给网络打印机进行打印。为了保证能够正常打印,操作系统为网络打印机生成一个“作业队列”,每个申请打印的“作业”应按先后的顺序排队,打印机从作业队列中逐个提取作业进行打印。
这方面的例子很多,在操作系统等后续课程中会涉及大量队列这种数据结构的应用。
在实际应用中,队列应用的例子更是常见,通常用以模拟排队情景。例如,拿汽车加油都要经过三段路程,第一段是在入口处排队等候进入加油车道;第二段是在加油车道排队等候加油;第三段是在进入出口处排队等候离开。实际上,这三段都是队列结构。若用算法模拟这个过程,总共需要设置的队列个数应为加油车道数加上2。
本章介绍了两种特殊的线性表:栈和队列,主要内容如下。
1)栈是限定尽在表尾进行插入和删除的线性表,又称为后进先出的线性表。栈有两种存储表示,顺序表示和链式表示。栈的主要操作是进栈和出栈,对于顺序栈的进栈和出栈操作要注意判断栈满或栈空。
2)队列是一种先进先出的线性表。它只允许在表的一端进行插入,而在另一端删除元素。队列也有两种存储表示,顺序表示和链式表示。队列的主要操作是进队和出队,对于顺序的循环队列的进队和出队操作要注意队满或队空。凡是涉及队头或队尾指针的修改都要将其对MAXQSIZE求模。
3)栈和队列是在程序涉及中被广泛使用的两种数据结构,其具体的应用场景都是与其表示方法和运算规则相互联系的。下表分别从逻辑结构、存储结构和运算规则3方面对二者进行了比较。
比较项目栈和队列 | 栈 | 队列 |
逻辑结构 | 和线性表一样,数据元素之间存在一对一的关系 | 和线性表一样,数据元素之间存在一对一的关系 |
存储结构 | 顺序存储: 存储空间预先分配,可能会导致空间闲置或栈满溢出现象;数据元素个数不能自由扩充 链式存储:动态分配,不会出现闲置或栈满溢出现象;数据元素个数可以自由扩充 |
顺序存储(常设计成循环队列): 存储空间预先分配,可能会导致空间闲置或队满溢出现象;数据个数不能自由扩充 链式存储: 动态分配,不会出现闲置或队满溢出现象;数据个数可以自由扩充 |
运算规则 | 插入和删除在表的一端(栈顶)完成,后进先出 | 插入运算在表的一端(队尾)进行,删除运算在表的另一端(队头),先进先出 |
4)栈有一个重要应用是在程序设计语言中实现递归。递归是程序设计中最为重要的方法之一,递归程序结构清晰,形式简洁。但递归程序在执行时需要系统提供隐式的工作栈来保存调用过程中的参数、局部变量和返回地址,因此递归程序占用内存空间较多,运行效率较低。