栈是一个重要应用是在程序设计语言中实现递归。递归是算法设计中最常用的手段,它通常将一个大型复杂问题的描述和求解变得简洁和清晰。因此递归算法常常比非递归算法更容易设计,尤其是当问题本身或所涉及的数据结构是递归定义的时候,使用递归方法更加合适。
3.4.1 采用递归算法解决问题
所谓递归是指,若在一个函数、过程或者数据结构定义的内部又直接或间接出现定义本身的应用,则称它是递归的,或者是递归定义的。
1、定义是递归的
有很多函数是递归定义的,如大家所熟悉的阶乘函数
对于阶乘函数,可以使用递归过程来求解
long Fact(long n)
{
if(n==0)
return 1; //递归终止条件
else
return n*Fact(n-1);
}
类似地,我们可以相应的写出斐波那契数列计算的表示方式
long Fib(long n)
{
if(n==1||n==2)
return 1;
else
return Fib(n-1)*Fib(n-2);
}
对于类似这种复杂的问题,若能够分解成几个相对简单且解法相同或类似的子问题来求解便称作递归求解。
在计算阶乘的时候,我们可以先求3!然后再求4!正如这样的计算步骤,我们可以采用“分治法”,进行递归求解的问题需要满足下面的三个条件。
(1)、能将一个问题转变成一个新问题,而新问题与原问题的解法相同或类同,不同的仅是处理的对象,并且这些处理对象更小且变化有规律。
(2)、可以通过上述转化使问题简化。
(3)、必须有一个明确的递归出口,或称递归的边界。
“分治法”求解递归问题算法的一般形式为
void p(参数表)
{
if(递归结束条件成立)可以直接求解; //递归终止的条件
else p(较小的参数) //递归步骤
}
可见,上述阶乘函数和Fibonacci数列的递归过程均与此一般形式相对应。
2、数据结构是递归的
某些数据结构本身具有递归的特性,则它们的操作可递归地描述。
例如,对于链表,其结点LNode的定义由数据域data和指针域next组成,而指针域next是一种指向LNode类型的指针,即LNode的定义中又用到了其自身,所以链表是一种递归的数据结构。
对于递归的数据结构,相应算法采用递归的方法来实现特别方便,链表的创建和链表的结点的遍历输出都可以采用递归的方法。算法3.9是从前向后遍历输出链表结点的递归算法,调用此递归函数之前,参数p指向单链表的首元结点,在递归的过程中,p不断地指向后继结点,直到p为NULL时递归结束。显然,这个问题满足上述给出的采用“分治法”进行递归求解的问题需要满足三个条件。
算法 3.9 遍历输出链表中各个结点的递归算法
【算法步骤】
1、如果p为NULL,递归结束返回
2、否则输出p->data,p指向后继结点继续递归
【算法描述】
void TraverseList(LinkList P)
{
if(p==NULL)
return; //递归终止
else
{
cout<<p->data<<end1; //输出当前结点的数据域
TraverseList (p->next); //p指向后继结点继续递归
}
}
在递归算法中,如果当递归结束条件成立,只执行return 操作时,“分治法”求解递归问题算法的一般形式可以简化为:
void p(参数表)
{
if(递归条件不成立)
p(较小的参数);
}
因此,算法3.9可以简化为:
void TraverseList(LinkList p)
{
if(p)
{
cout<<p->data<<end1;
TraversList(p->next);
}
}
后面章节要介绍的广义表、二叉树等也是典型的具有递归特性的数据结构,其相应算法也可以采用递归的方法来实现。
3、问题的解法是递归的
还有一类问题,虽然问题本身没有明显的递归结构,但用递归求解比迭代求解更加简单,如Hanoi塔问题、八皇后问题、迷宫问题等。
算法 3.10 Hanoi塔问题的递归算法
【算法步骤】
1、如果n=1,则直接将编号为1的圆盘从A移到C,递归结束。
2、否则:
(1)、递归,将A上编号为1至n-1的圆盘移到B,C做辅助塔;
(2)、直接将编号为n的圆盘从A移到C
(3)、递归,将B上编号为1至n-1的圆盘移到C,A做辅助塔
【算法描述】
void Hanoi(int n,char A,char B,char C)
{ //将塔座A上的n个圆盘按规则搬到C上,B做辅助塔
if(n==1)
move(A,1,c);
else
{
Hanoi(n-1,A,C,B);//将A上编号为1至n-1的圆盘移到B,C做辅助塔
move(A,n,C);//将编号为n的圆盘从A移到C
Hanoi(n-1,B,A,C);//将B上编号为1至n-1的圆盘移到C,A做辅助塔
}
}
3.4.2 递归过程与递归工作栈
一个递归函数,在函数执行的过程中,需多次进行自我调用。那么这个递归函数是如何执行的呢?先看任意两个函数之间进行调用的情形。
与汇编语言程序设计中主程序和子程序之间的链接和信息交换相类似,在高级语言编制的程序中,调用函数和被调用函数之间的链接及信息交换需要通过栈来完成。
通常,当在一个函数的运行期间调用另外一个函数时,在运行被调函数之前,系统需先完成三件事:
(1)、将所有的实参、返回地址等信息传递给被调函数保存
(2)、为被调用函数的局部变量分配存储区
(3)、将控制转移到被调函数的入口
而从被调用函数返回调用函数之前,系统也应该完成三项操作:
(1)、保存被调函数的计算结果
(2)、释放被调函数的数据区
(3)、依照被调函数保存的返回地址将控制转移到调用函数
当有多个函数构成嵌套调用时,按照“后调用先返回”的原则,上述函数之间的信息传递和控制转移必须通过“栈”来实现,即系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区,每当从一个函数退出时,就释放它的存储区,则当前运行的函数的存储区必在栈顶。
例如,在图3.9(c)所示的主函数中,调用了函数first。而在函数first中又调用了函数second,则图3.9(a)所示为当前正在执行函数second中某个语句中栈的状态,而图3.9(b)展示从函数second退出之后正执行函数first中某个语句时栈的状态。
一个递归函数的运行过程类似于多个函数的嵌套调用,只是调用函数与被调用函数是同一个函数,因此,和每次调用相关的一个重要概念是递归函数运行的“层次”。假设调用该递归函数的主函数为0层,则从主函数调用递归函数为进入1层;从第i层递归调用本函数为进入“下一层”,即第i+1层。反之,退出第i层递归应返回至“上一层”,即第i-1层。为了保证递归函数正确执行,系统需设立一个“递归工作栈”,作为整个递归函数运行期间使用的数据存储区。每一层递归所需信息构成一个工作记录,其中包括所有的实参、所有的局部变量,以及上一层的返回地址,每进行一次递归,就产生一个新的工作记录压入栈顶。每退出一层递归时,就从栈顶弹出一个工作记录,称这个记录为“活动记录”
3.4.3 递归算法的效率分析
1、时间复杂度的分析
在算法分析中,当一个算法中包含递归调用时,其时间复杂度的分析可以转换为一个递归方程求解。实际上,这个问题是数学上求解渐进阶的问题,而递归方程的形式多种多样,其求解方法也不一而足。迭代法是求解递归方程中的一个常用的方法,其基本步骤是迭代地展开递归方程的右端,使之成为递归方程的一个和式,然后通过对和式的估计来达到对方程左端的估计(即方程的解的估计)。
下面以阶乘的递归函数Fact(n)为例,说明通过迭代法求解递归方程来计算时间复杂度的方法。
设Fact(n)的执行时间为T(n),此递归函数中语句if(n==0)return 1;的执行的时间复杂度为O(1),递归调用Fact(n-1),所以else return n*Fact(n-1);的执行时间是O(1)+T(n-1)。其中,设两个数的相乘和赋值操作的时间复杂度为O(1),则对某常数C、D有如下递归方程:
T(n)=1、D n=0。2、C+T(n-1) n>=1。
2、时间复杂度的分析
递归函数在执行时,系统需建立一个“递归工作栈”存储每一层递归所需的信息,此工作栈是递归函数执行的辅助空间,因此,分析递归算法的空间复杂度需要分析栈的大小。
对于递归算法,空间复杂度:
S(n)=O(f(n))
其中,f(n)为“递归工作栈”中工作记录的个数和问题规模n的函数关系。
根据这种分析方法不难得到,前面讨论的阶乘问题,Fibonacci数列问题、Hanoi塔问题的递归算法的空间复杂度均为O(n)。
3.4.4 利用栈递归转换为非递归的方法
通过上述讨论,可以看出递归程序在执行的过程中需要系统提供隐式栈这种数据结构来实现,对于一般的递归方程,仿照递归算法执行过程中递归工作栈的状态变化可直接写出相应的非递归算法。
这种利用栈消除递归过程的步骤如下:
1、设置一个工作栈存放递归工作记录(包括实参、返回地址及局部变量等)。
2、进入非递归调用入口,将递归调用的程序传来的实在参数和返回地址入栈,(递归程序不可以作为主程序,因而可认为初始是被某个调用程序调用)
3、进入递归调用入口:当不满足递归结束条件时,逐层递归,将实参、返回地址、及局部变量入栈,这一过程可以用循环语句来实现——模拟递归分解的方程。
4、递归结束条件满足,将到达递归出口的给定常数作为当前的函数值。
5、返回处理:在栈不空的条件下,反复退出栈顶记录,根据记录中返回地址进行题义规定的操作,即逐层计算当前函数值,直至栈为空为止——模拟递归求值过程。
通过上述步骤,可将任何递归算法改写成非递归算法。但改写后的非递归算法和原来的比较起来,结构不够清晰,可读性差,有的还需要经过一系列的优化,如二叉树中序遍历的非递归算法。
由于递归函数清晰,程序易读,而且其正确性容易得到证明,因此,利用允许递归调用的语言(如C语言)进行程序设计的时候,给用户编制程序和调试程序带来很大方便。因为对于这样一类递归问题编程时,不需要用户自己由系统来管理递归工作栈。
3.5.1 队列的类型定义
队列的操作与栈的操作类似,不同的是,删除是在表的头部(即表头)进行。下面给出队列的抽象数据类型定义:
ADT Queue {
数据对象 :D={ai|ai∈ElemSet,i=1,2,3,...,n,n>=0}
数据关系:R={<ai-1,ai>|ai-1,ai∈D,i=1,2,....,n}
//在其中约定ai端为队列头,an端为队列尾。
基本操作:
InitQueue(&Q)
操作结果:构造一个空队列Q
DestroyQueue(&Q)
初始条件:队列Q已存在
操作结果:队列Q被销毁,不再存在
ClearQueue(&Q)
初始条件:队列Q已存在
操作结果:将Q清空为空队列
QueueEmpty(Q)
初始条件:队列Q已存在
操作结果:若Q为空队列,则返回true,否则返回false
GetHead(Q)
初始条件:队列Q已存在
操作结果:返回Q的队头元素
EnQueue(&Q,e)
初始条件:队列Q已存在
操作结果:插入新的元素e为Q的新的队尾元素
DeQueue(&Q,&e)
初始条件:队列Q已存在
操作结果:删除Q的队头元素,并用元素e返回其值
QueueTraverse(Q)
初始条件:Q已存在且非空
操作结果:从队头到队尾,依次对Q的每个数据元素进行访问
}ADT Queue
和栈类似,在本书后面内容中引用的队列都是如上定义的队列类型,队列的数据元素类型在应用程序内定义。
3.5.2 循环队列——队列的顺序表示和实现
队列也有两种存储表示,顺序表和链表表示。和顺序栈相似,在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队头到队列尾的元素之外,尚需附设两个整型变量front和rear分别指示队头元素及队列尾元素的位置(后面分别称为头指针和尾指针)。队列的顺序存储结构表示如下:
//-------队列的顺序存储结构---------
#define MAXSIZE 100
typedef struct
{
QElemType *base; //存储空间的基地址
int front; //头指针
int rear; //尾指针
}SqQueue;
为了在C语言中描述方便起见,在此约定:初始化建立空队列时,令front=rear=0,每当插入新的队列尾元素时,尾指针rear增1,。因此,在非空队列中,头指针始终指向队头元素,而尾指针始终指向队列尾元素的下一个位置。
我们需要知道,对于循环队列不能以头、尾指针的值是否相同来判断队列空间是“满”还是“空”,然而,在这种情况下,如何区别队满还是队空呢?
通常有以下两种处理方法。
1、少用一个元素的空间,即队列空间大小为m的时候,有m-1个元素的时候就认为队是满的。这样判断队空的条件不变,即当头、尾指针的值相同时,则认为队空;而当尾指针在循环意义上加1后是等于头指针,则认为队满。因此,在循环队列中队满和队空的条件是:
队空的条件:Q.front=Q.rear
队满的条件:(%.rear+1)%MAXSIZE==Q.front
1.初始化
循环队列的初始化操作就是动态分配一个预定义大小为MAXSIZE的数组空间。
算法 3.11 循环队列的初始化
【算法步骤】
1、为队列分配一个最大容量的数组空间,base指向数组空间的首地址。
2、头 指针和尾指针置为0
【算法描述】
StatusQueue(SqQueue &Q)
{ //构造一个空队列Q
Q.base=new QElemType[MAXSIZE]; //为队列分配一个最大容量为MAXSIZE的空间
if(!Q.base)
exit(OVERFLOW);//存储分配失败
Q.front=Q.rear=0; //头指针和尾指针置为0,队列为空
return OK;
}
2、求队列长度
对于非循环队列,尾指针和头指针的差值便是队列长度,而对于循环链表,差值可能是负数,所以需要将差值加上MAXSIZE,然后与MAXSIZE求余。
算法 3.12 求循环队列的长度
【算法描述】
int QueueLength(SqQueue Q)
{ //返回Q的元素个数,即队列的长度
return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}
3. 入队
入队是指在队尾插入一个新的元素。
算法 3.13 循环队列的入队
【算法步骤】
1、判断队列是否满,若满则返回ERROR。
2、将新元素插入队尾
3、队尾指针加1
【算法描述】
Status EnQueue(SqQueue &Q,QElemType e)
{ //插入元素e为Q的新的队尾元素
if((Q.rear+1)%MAXSIZE==Q.front) //尾指针循环意义上加1后等于头指针
return ERROR;
Q.base[Q.rear]=e; //新元素插入队尾
Q.rear=(Q.rear+1)%MAXSIZE; //队尾指针加1
return OK;
}
4 . 出队
出队操作是将队头元素删除。
算法 3.14 循环队列的出队
【算法步骤】
1、判断队列是否为空,若空,则返回ERROR
2、保存队头元素
3、队头指针加1
【算法描述】
Status DeQueue(SqQueue &Q,QElemType &e)
{ //删除Q的队头元素,并用e返回其值
if(Q.front==Q.base)
return ERROR; //队空
e=Q.base[Q.front]; //保存队头元素
Q.front=(Q.front+1)%MAXSIZE; //队头指针加1
return Ok;
}
5.取队头元素
当对列非空的时候,此操作返回当前队头元素的值,队头指针保持不变。
算法 3.15 取循环队列的队头元素
【算法描述】
SElemType GetHead(SqQueue Q)
{ //返回Q的队头元素
if(Q.front!=Q.rear) //队列非空
return Q.base[Q.front]; //返回队头元素的值,队头指针不变
}
由上述分析可知,如果用户的应用程序中设有循环队列,则必须为它设定一个最大对列长度。若用户无法预估所用对列的最大长度,则宜采用链队。
3.5.3 链队——队列的链式表示和实现
链队是指采用链式存储结构实现的队列,通常链队用单链表表示,一个链队显然需要两个分别指示队头和队尾的指针(称为头指针和尾指针)才能唯一确定。这里和线性表的单链表一样,为了使操作更为方便,给链队增加一个头结点,并令头指针始终指向头结点。对列的链式存储结构如下:
//---------队列的链式存储结构------------------
typedef struct QNode
{
QElemType data;
struct QNode *next;
}QNode,*Queueptr;
typedef struct
{
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
}LinkQueue;
链队的操作即为单链表插入和删除操作的特殊情况,只是进一步修改尾指针和头指针。
下面给出链队初始化、入队、出队操作的实现。
1、初始化
链表的初始化就是构造只有一个头结点的空队。
算法 3.16 链表的初始化
1、生成新结点作为头结点,队头和队尾指针指向此结点
2、头结点的指针域置空
【算法描述】
Status InitQueue(LinkQueue &Q)
{ //构造一个空队列Q
Q.front=Q.rear=new QNode; //生成新结点作为头结点队头队尾指针指向此结点
Q.front->next=NULL; //头结点的指针域置空
return Ok;
}
2 、入队
和循环队列的入队操作不同的是,链队在入队前不需要判断队是否满,需要为入队元素动态分配一个结点空间。
算法 3.17 链队的入队
【算法步骤】
1、为入队元素分配结点空间,用指针p指向
2、将新结点的数据域置为e
3、将新结点插入到队尾
4、修改队尾指针为p
【算法描述】
Status EnQueue(LinkQueue &Q,QElemType e)
{ //插入元素e为新的队尾元素
p=new QNode; //为入队元素分配结点空间,用指针p指向
p->data=e; //将新结点的数据域置为e
p->next=NUll;
Q->rear=p; //将新结点插入到队尾
Q.rear=p; //修改队尾指针
return OK;
}
3、出队
和循环队列一样,链队在出队前也需要判断队列是否为空,不同的是,链队在出队之后需要释放队头元素所占的内存空间。
算法 3.18 链队的出队
【算法步骤】
1、判断队列是否为空,若空则返回ERROR
2、临时保存队头元素的空间,以备释放
3、修改头结点的指针域,指向下一个结点
4、判断出队元素是否为最后一个元素,若是,则将队尾指针重新赋值,指向头结点
5、释放原队头元素的空间
【算法描述】
Status DeQueue(LinkQueue &Q,QElemType &e)
{ //删除Q的队头元素,用e返回其值
if(Q.front==Q.rear)
return ERROR; //若队为空,则返回ERROR
p=Q.front->next; //p指向队头元素
e=p->data; //e用于保存队头元素的值
Q.front->next=p->next; //修改头结点的指针域
if(Q.rear==p)
Q.rear=Q.front; //最后一个元素被删,队尾指针指向头结点
delete p; //释放原队头元素的空间
return OK;
}
需要注意的是,在链队出队操作的时候还要考虑当队列中最后一个元素被删后,队列尾指针也丢失了,因此需对队尾指针重新赋值(指向头结点)。
4、取队头元素
与循环队列一样,当对列非空的时候,此操作返回当前队头元素的值,队头指针保持不变。
算法 3.19 取链队的队头元素
【算法描述】
SElemType GetHead(LinkQueue Q)
{ //返回Q的队头元素,不修改头指针
if(Q.front!=Q.rear) //队头非空
return Q.front->next->data; //返回队头元素的值,队头指针不变
}
案例 3.1 数制的转换
【案例分析】
当将一个十进制整数转换为八进制数时,在计算过程中,把n与8求余得到的八进制数的各位依次进栈,计算完毕后,将栈的八进制数依次出栈输出,输出结果就是待求得的八进制数。
【案例分析】
在具体实现时,栈可以采用顺序存储表示也可以采用链式存储表示。
算法 3.20 数制的转换
【算法步骤】
1、初始化一个空栈S
2、当十进制数N非零时,循环执行以下操作:
(1)、把N与8求余得到的八进制数压入栈S
(2)、N更新为上次N与8的商
3、当栈非空时,循环执行以下操作:
(1)、弹出栈顶元素e
(2)、输出e
【算法描述】
void conversion(int n)
{ //对于任意一个非负十进制数,打印输出与其等值的八进制数
InitStack(S); //初始化空栈S
while(N)
{
Push(S,N%8);
N=N/8;
}
while(!StackEmpty(S)) //当栈S非空时,循环
{
Pop(S,e); //弹出栈顶元素e
cout<<e; //输出e
}
}
【算法分析】
显然,该算法的时间和空间复杂度为O(log8n)。
这是利用栈的后进先出的特性的最简单的例子,在这个例子中,栈的操作是单调的,即先一味地入栈,然后一味地出栈,也许会觉得:用数组实现不是更简单吗?但仔细分析上面算法可以看出,栈的引入简化了程序设计的问题,划分了不同的关注层次,使思考范围缩小了,使思考范围缩小了,而用数组不仅掩盖了问题的本质,还要分散精力去考虑数组下标增减等细节问题。
在实际利用栈的过程中,入栈和出栈操作大都不是单调的,而是交错进行的,下面是案例所呈现:
案例3.2 : 括号匹配的实验
案例分析:
检验算法借助一个栈,每当读入一个左括号,则直接入栈,等待相匹配的同类右括号;每当读入一个右括号,若与当前栈顶的左括号类型相同,则二者匹配,将栈顶的左括号出栈,直到表达式扫描完毕。
在处理过程中,还要考虑括号不匹配出错的情况。例如,当出现**(()[])这种情况时,由于前面入栈的左括号均与和右面出现的右括号相匹配,栈已空,因此最后扫描的右括号不能得到匹配;出现(([])这种错误,当表达式扫描结束的时候,栈中还有一个左括号没有匹配,出现(()]**这种情况
显然是栈顶的左括号和最后面的右括号不匹配造成的。
【案例实现】
算法3.21 括号的匹配
【算法步骤】
1、初始化一个空栈S。
2、设置一个标志性变量flag,用来标记匹配结果以控制循环及其返回结果,1表示正确匹配,0表示错误匹配,flag初值为1。
3、扫描表达式,依次读入字符ch,如果表达式没有扫描完毕且flag非零,则循环执行以下操作:
(1)、若ch是左括号“[”或“(”,则将其压入栈
(2)、若ch是右括号“)”,则根据当前栈顶元素的值分情况考虑:若栈非空且栈顶元素是“(”,则正确匹配,否则错误匹配,flag置为0.
(3)、若ch是右括号“]”,则根据当前栈顶元素的值分情况考虑:若栈非空且栈顶元素是“[”,则正确匹配,否则错误匹配,flag置为0。
4、退出循环后,如果栈空且flag的值为1,则匹配成功,返回true,否则返回false。
【算法描述】
Status Matching()
{//检验表达式中所含括号是否正确匹配,是则返回true,否则返回false
//表达式以“#”结束
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;
} //switch
cin>>ch; //继续读入下一个字符
} //while
if(StackEmpty(S)&&flag)
return true;
else
return false;
}
【算法分析】
此算法从头到尾扫描表达式中每个字符,若表达式的字符串长度为n,则此算法的时间复杂度为O(n)。算法在运行时所占用的辅助空间主要取决于S栈的大小,显然,S栈的空间大小不会超过n,所以,此算法的空间复杂度为O(n)。
本次介绍了两种特殊的线性表:栈和队列,主要内容如下:
(1)、栈是限定仅在表尾进行插入和删除操作的线性表,又称为后进先出的线性表。栈有两种存储表示,顺序表示(顺序栈)和链式表示(链栈)。栈的主要操作是进栈和出栈,对于顺序表的进栈和出栈操作要注意栈满或栈空。
(2)、队列是一种先进先出的线性表,它只允许在表的一端进行插入,而在另一端删除元素,队列也有两种存储表示,顺序表(循环队列)和链式表示(链队)。队列的主要操作是进队和出队,对于顺序的循环队列的进队和出队操作要注意判断栈满或栈空。凡是涉及队头或队尾指针的修改都要将其对MAXSIZE求模。
(3)、栈和队列是在程序设计中被广泛使用的两种数据结构,其具体的应用场景都是与其表示方法和运算规则相互联系的。
栈和线性表一样,数据元素之间存在着一一对应的关系
栈的顺序存储结构:存储空间预先分配,可能会导致空间闲置或栈满溢出的现象;数据元素个数不能自由扩充
栈的链式存储结构:是动态分配的,不会出现闲置或栈满溢出的现象,数据元素个数可以自由扩充。
队列和线性表一样,数据元素之间存在着一一对应的关系
对列的顺序存储结构与栈的相似,存储空间预先分配,可能会导致空间闲置或栈满溢出的现象,且数据元素个数不能自由扩充。
队列的链式存储结构与栈相似,是动态分配的,不会出现闲置或队满溢出的现象,且数据元素个数不可以自由扩充。
栈的运算规则是插入和删除在表的一端,后进先出
对列的运算规则是插入在队尾,删除在队头,先进先出
(4)、栈有一个重要应用是在程序设计语言中实现递归。递归是程序设计中最为重要的方法之一,递归结构清晰,形式简洁,但递归程序在执行时间需要系统提供隐式的工作栈来保存调用过程中的参数、局部变和返回地址,因此递归程序占用的内存空间较多,运行效率低。