第3章
栈 和 队 列




【本章学习目标】

栈和队列是在程序设计中被广泛使用的两种线性数据结构。栈只允许在表的一端进行插入或删除操作,而队列只允许在表的一端进行插入操作在另一端进行删除操作。因而,栈和队列也可以被称作操作受限的线性表。通过本章的学习,要求: 

 理解栈和队列的特点; 

 熟练掌握顺序栈和链栈基本操作的算法实现; 

 熟练掌握循环队列和链队列基本操作的算法实现; 

 掌握栈和队列的应用。

3.1栈
3.1.1栈的引例

为了说明栈的概念,举一个简单的例子。把餐馆中洗净的一叠盘子看作一个栈。通常情况下,最先洗净的盘子总是放在最下面,后洗净的盘子放在先洗净的盘子上面,最后洗净的盘子总是放在最上面; 使用时,总是先从顶上取走,也就是说,后洗净的盘子先取走,先洗净的盘子后取走,即所谓的“先进后出”。栈的应用非常广泛,对高级语言中表达式的处理就是通过栈来实现的。在程序设计中,如果需要以与保存数据时相反的顺序来使用数据,就可以用栈来实现。

3.1.2栈的类型定义




图3.1栈的示意图


栈(Stack)是限定在表的一端进行插入和删除操作的线性表。允许插入和删除的一端称作栈顶(Top),固定的另一端称作栈底(Bottom)。当栈中没有元素时称为空栈。

如图3.1所示,栈中有n个元素,进栈的顺序是a0,a1,…,an-1,当需要出栈时,其顺序为an-1,…,a1,a0,所以栈又称为后进先出(Last In First Out)的线性表,简称LIFO表。

对于栈,有以下几种基本运算。

(1) 栈的初始化Init_Stack(s): 构造一个空栈。

(2) 判栈空Empty_Stack(s): 若栈s为空栈,则函数返回值为1,否则返回值为0。

(3) 入栈Push_Stack(s,x):  在栈s的顶部插入一个新元素x,x成为新的栈顶元素,栈发生变化。

(4) 出栈Pop_Stack(s):  将栈s的顶部元素从栈中删除,栈中少了一个元素,栈发生变化。

(5) 读栈顶元素Top_Stack(s): 返回栈顶元素,栈不变化。

(6) 求栈的长度StackLength(s): 返回栈s中的元素个数。

3.1.3栈的顺序存储表示和操作的实现

与第2章讨论的一般顺序存储结构的线性表一样,利用一组地址连续的存储单元依次存放从栈底到栈顶的数据元素,这种形式的栈称为顺序栈。因此,可以使用预设的足够长度的一维数组datatype data[MAXSIZE]来实现,将栈底设在数组小下标的一端,由于栈顶位置是随着元素的进栈和出栈操作而变化的,因此用一个指针int top指向栈顶元素的当前位置。用C语言描述顺序栈的数据类型如下: 

#define datatypechar

#define MAXSIZE100 

typedefstruct

{datatypedata[MAXSIZE];

inttop;

}SEQSTACK;
定义一个指向顺序栈的指针: 

SEQSTACK*s;
MAXSIZE是栈s的最大容量。鉴于C语言中数组的下标约定是从0开始的,因而使用一维数组作为栈的存储空间时,应设栈顶指针s->top=-1时为空栈。元素入栈时,栈顶指针加1,即s->top++; 元素出栈时,栈顶指针减1,即s->top--。当top等于数组的最大下标值时则栈满,即s->top=MAXSIZE-1。图3.2说明了顺序栈中数据元素与栈顶指针的变化。这里设MAXSIZE=5,图3.2(a)是空栈状态,图3.2(b)是元素A入栈后的状态,图3.2(c)是栈满状态,图3.2(d)是在图3.2(c)之后E、D相继出栈,此时栈中还有3个元素,或许最近出栈的元素D、E仍然在原先的单元存储着,但top指针已经指向了新的栈顶,则元素E、D已不在栈中了。



图3.2栈顶指针top与栈中数据元素的关系


在上述存储结构上基本操作的算法实现如下: 

1. 初始化空栈操作

voidInit_Stack(SEQSTACK *s)/*创建一个空栈由指针s指出*/

{ s->top=-1;

}
2. 判栈空操作

int Empty_Stack (SEQSTACK *s)/*栈s空时,返回1; 非空时,返回0*/

{

if(s->top==-1) 

return 1; 

else

return 0;

}
3. 入栈操作

voidPush_Stack(SEQSTACK *s,datatype x)/*将元素x插入栈s中,作为s的新栈顶*/

{

if(s->top==MAXSIZE-1)/*栈满*/

printf("Stack full\n");

else 

{ s->top++;

s->data[s->top]=x;

}

}
4. 出栈操作

datatypePop_Stack(SEQSTACK *s )/*若栈s不为空,则删除栈顶元素*/

{

datatypex;

if(Stack_Empty(s))/*栈空*/

{ printf(" Stack empty\n"); 

return NULL;}

else 

{x=s->data[s->top]; 

s->top--;

return x;}

}
5. 读栈顶元素操作

datatype Top_Stack(SEQSTACK *s)/*若栈s不为空,则返回栈顶元素*/

{

datatype x;

if(Stack_Empty(s))/*栈空*/

{ printf("Stack empty.\n");

return NULL;}

else

{x=s->data[s->top];

return x;} 

}
6. 求栈的长度操作

int StackLength(SEQSTACK *s)

{

return s->top+1;

}
对于其他类型栈的基本操作的实现,只需改变结构体SEQSTACK中的数组data[MAXSIZE]的基本类型,即对datatype重新进行宏定义。

对于栈的顺序存储结构的两点说明如下。

(1) 对于顺序栈,入栈时,首先判断栈是否满,栈满的条件为s->top==MAXSIZE-1,栈满时不能入栈,否则会出现空间溢出,引起错误,这种现象称为上溢。

(2) 出栈和读栈顶元素时,要先判断栈是否为空,为空时不能操作,否则产生错误(或称为下溢)。通常栈空常作为一种控制转移的条件。

3.1.4栈的链式存储表示和操作的实现

栈也可以采用链式存储结构表示,这种链式存储结构的栈称为链栈。用C语言描述链栈的数据类型如下: 

typedef struct Stacknode 

{

datatype data;

struct Stacknode *next;

}LINKSTACK;
定义一个栈顶指针: 
LINKSTACK *top;


图3.3链栈示意图


在一个链栈中,栈底就是链表的最后一个结点,而栈顶总是链表的第一个结点。栈顶指针top唯一确定一个链栈,当top=NULL时,该链栈是一个空栈。新入栈的元素成为链表的第一个结点,只要系统还有存储空间,就不会有栈满的情况发生。图3.3给出了链栈中数据元素与栈顶指针top的关系。


链栈基本操作的算法实现如下。

1. 初始化空栈

void Init_Stack(LINKSTACK *top)

{

top=NULL;

}
2. 判栈空操作

int Empty_Stack(LINKSTACK*top)/*栈s为空时,返回1; 非空时,返回0*/

{ 

if(top==NULL)

return 1;

else

return 0;

}
3. 入栈操作

voidPush_Stack(LINKSTACK*top, datatype x)/*将元素x压入链栈top中*/

{

LINKSTACK *p;

p=(LINKSTACK *)malloc(sizeof(LINKSTACK));/*申请一个结点*/

p->data=x; 

p->next=top;

top=p;

}
4. 出栈操作

datatypePop_Stack(LINKSTACK*top)/*从链栈top中删除栈顶元素*/

{

datatype x;

LINKSTACK *p;

if (top==NULL)/*空栈*/

{printf(" Stack empty\n");

return NULL;}

else 

{ p=top; 

top=top->next; 

x=p->data;

free(p);

return x;

}

}
5. 读栈顶元素操作

datatype Top_Stack(LINKSTACK *top)/*若栈s不为空,则返回栈顶元素*/

{

datatype x;

if (top==NULL)/*空栈*/

{printf(" Stack empty\n");

return NULL;}

else 

{ x=top->data;

return x;}

}
3.2栈 的 应 用

【例3.1】编写算法,将十进制正整数m转换为n进制数。

算法分析: 利用“辗转相除法”,即不断地用m除以n,直到m=0为止,将各项除得的余数倒排。这里将十进制数m除以n所得的各位余数入栈,然后依次出栈并输出即可。

void zhuan(int m,int n)

{int i;

SEQSTACK *S;

S- >top=-1;

While(m!=0)

{Push(s,m%n);

m=m/n;}

For(i=S->top;i>=0;i- -)

Printf("%d",s->data[i]);

}

【例3.2】编写算法,利用栈将带头结点的单链表逆置。

算法分析: 把单链表S1看成一个链栈,将从S1出栈的元素依次入栈S2,直到S1为空为止,此时S2就是S1的逆置。

Void nizhi(LinkStack *S1,LinkStack *S2)

{int x;

While(S1->next!=NULL)

{x=Pop(S1);

Push(S2,x);}}

3.3队列
3.3.1队列的引例

和栈一样,队列也是一种特殊的线性表。对于队列我们并不陌生,商场、银行的柜台前需要排队,餐厅的收款机旁需要排队。这里我们来看一个简单的事件排队问题,用户可以输入和保存一系列事件,每个新事件只能在队尾插入; 每次先处理队头事件,即先输入和保存的事件,当队头事件处理完毕后,它就会从事件队列中被删除; 还可以查询事件队列中剩余的事件。

3.3.2队列的定义及其基本操作

队列(Queue)也是一种运算受限的线性表。它只允许在表的一端进行插入,在另一端进行删除。允许插入的一端称为队尾(Rear),允许删除的一端称为队头(Front)。在队列中,先进入队列的成员总是先离开队列。因此,队列也称作先进先出(First In First Out)的线性表,简称FIFO表。图3.4显示了队列的这种特点。

当队列中没有元素时,称为空队列。在空队列中依次加入元素a1,a2,…,an之后,a1称为队头元素,an称为队尾元素。图3.4所示是一个有n个元素的队列,入队的顺序依次为素a1,a2,…,an,出队时的顺序将依然是a1,a2,…,an。



图3.4队列示意图


对于队列,有以下几种基本运算。

(1) 队列初始化Init_Queue(q): 构造了一个空队列。

(2) 判队列空Empty_Queue(q): 若队列q为空队列,则函数返回值为1,否则返回值为0。

(3) 入队列Add_Queue(q,x):  在队列q的尾部插入一个新元素x,x成为新的队尾。

(4) 出队列Del_Queue(q):  删除队头元素,并返回该队头元素,队列发生变化; 若队列空,则函数返回值为0。

(5) 读队头元素Front_Queue(q): 返回队头元素,队列不变; 若队列空,则函数返回值为0。

(6) 求队列的长度Length_Queue(q): 返回队列q中的元素个数。

3.3.3队列的顺序存储表示和操作的实现

队列是一种特殊的线性表,因此队列可采用顺序存储结构存储,也可以使用链式存储结构存储。利用一组地址连续的存储单元依次存放队列中的数据元素,这种形式的队列称为顺序队列。一般情况下,使用一维数组作为队列的顺序存储空间,另外再设立两个指针: 一个为指向队头元素位置的指针front; 另一个为指向队尾元素位置的指针rear。随着元素的入队和出队操作,front和rear是不断变化的。用C语言描述顺序队列的数据类型如下: 

#definedatatypechar

#defineMAXSIZE100/*队列的最大容量*/

typedefstruct

{ datatype data[MAXSIZE];/*队列的存储空间*/

intfront,rear;/*队头队尾指针*/

}SEQUEUE;
定义一个指向顺序队列的指针变量: 

SEQUEUE*q;
下面分析队列的基本操作的实现。设MAXSIZE=5,C语言中,数组的下标是从0开始的,因此为了算法设计的方便,我们约定: 初始化队列时,q->front=q->rear=-1。图3.5(a)所示为初始化空队列的情况。在队列中插入一个元素x时,即在队尾插入,则有操作q->rear++;q->data[q->rear]=x。图3.5(b)所示为在队列中依次插入2个元素A、B后的情况,此时尾指针rear始终指向队尾元素所在位置,头指针front指向队列中第一个元素的前面一个位置。出队操作,即从队头删除一个数据元素,则有q->front++,如图3.5(c)所示,在图3.5(b)之后元素A、B依次出队列,队列空,此时队头指针发生变化,队尾指针不变。由此可见,当front和rear等于数组的同一个下标值时队列空,即q->front=q->rear。图3.5(d)是在图3.5(c)之后,C、D、E相继插入队列,队列满,此时队尾指针q->rear=MAXSIZE-1,队头指针不变。由此可见,当rear等于数组的最大下标值时,则队列满,即q->rear=MAXSIZE-1。



图3.5顺序队列基本操作分析图示




图3.6循环队列示意图


随着入队出队操作的进行,整个队列整体向后移动,这样就出现了图3.5(d)所示的现象: q->rear=4,给出了队列满信号,再有元素入队就会出现溢出,而事实上此时队列中并未真的“满员”,这种现象为“假溢出”,这是由于“队尾入,队头出”这种受限操作造成的。解决这一问题的常用方法是采用循环队列,将队列存储空间的最后一个位置绕到第一个位置,即将q->data[0]接在q->data[MAXSIZE-1]之后,形成逻辑上的环状空间,如图3.6所示。


在循环队列中,每插入一个新元素时,就把队尾指针rear沿顺时针方向移动一个位置。即 

q->rear=q->rear+1;

if(q->rear==MAXSIZE) q->rear=0;

或改成一种更简洁的描述方式: 

q->rear=(q->rear+1)%MAXSIZE;

在循环队列中,每删除一个元素时,就把队头指针front沿顺时针方向移动一个位置。即 

q->front=q->front+1;

if(q->front==MAXSIZE) q->front=0;

或改成一种更简洁的描述方式:

q->front=(q->front+1)%MAXSIZE;

从图3.7所示的循环队列中可以看出,图3.7(a)中有A、B、C三个元素,此时front=2、rear=0; 随着元素D、E相继入队,队中有5个元素,队列满情况出现,此时 front=2、rear=2,有q->front=q->rear,如图3.7(b)所示。若在图3.7(a)情况下,元素A、B、C相继出队,队列空情况出现,此时front=0、rear=0,也有q->front=q->rear,如图3.7(c)所示。上述队列满和队列空情况出现的过程不同,但队列满和队列空情况的结果相同。这显然是必须要解决的一个问题。



图3.7循环队列队列空、队列满条件分析


解决这个问题的方法有很多种。一种简单的方法是: 损失一个元素空间不用,即当循环队列中元素的个数是MAXSIZE-1时就认为队列满了,图3.7(d)所示的情况就是队列满,这样队列满的条件变为(q->rear+1) % MAXSIZE=q->front,也能和队列空区别开。

现将循环队列的基本操作的准则总结如下。

循环队列的初始化条件: q->front=q->rear=0。

循环队列的队列满条件: (q->rear+1) % MAXSIZE=q->front。

循环队列的队列空条件: q->front=q->rear。

下面给出循环队列基本操作的算法实现。

1. 初始化队列

voidInit_Queue(SEQUEUE *q)/*创建一个空队列由指针q指出*/

{

q->front=0;

q->rear=0;

}
2. 判队列空操作

intEmpty_Queue(SEQUEUE*q)/*队列q为空时,返回1; 非空时,返回0*/

{ 

if (q->front==q->rear)

return 1;

else 

return 0;

}
3. 入队列操作

voidAdd_Queue(SEQUEUE *q,datatypex)/*将元素x插入队列q中,作为q的新队尾*/

{

if ((q->rear+1) % MAXSIZE==q->front)/*队列满*/

printf(" Queue full\n");

else

{q->rear=(q->rear+1) % MAXSIZE;

q->data[q->rear]=x;

}

}
4. 出队列操作

datatypeDel_Queue(SEQUEUE *q)/*若队列q不为空,则删除队头元素,并返回队头元素*/

{

datatypex;

if (Empty_Queue(q))/*队列为空*/

{ printf(" Queue empty\n"); 

return NULL;}

else

{q->front=(q->front+1) % MAXSIZE;

x= q->data[q->front];

return x; }

}
5. 读队头元素操作

datatypeFront_Queue(SEQUEUE *q)/*若队列q不为空,则返回队头元素*/

{

datatypex;

if (Empty_Queue(q))/*队列为空*/

{ printf(" Queue empty\n"); 

return NULL;}

else

{x= q->data[(q->front+1) % MAXSIZE];

return x; }

}
6. 求队列长度操作

int Length_Queue(SEQUEUE *q)/*返回队列q的元素个数*/

{

int len;

len=(MAXSIZE+q->rear-q->front)%MAXSIZE;

return len;

}
3.3.4队列的链式存储表示和操作的实现

队列也可以采用链式存储结构表示,这种链式存储结构的队列称为链队列。用C语言描述链队列的数据类型如下: 

typedef struct Queuenode

{ datatypedata;

structQueuenode *next;

} Linknode;/*链队结点的类型*/

typedef struct 

{ Linknode *front,*rear;

}LINKQUEUE;/*将头尾指针封装在一起的链队列*/
定义一个指向链队列的指针: 

LINKQUEUE*q;
为了操作方便,和线性链表一样,也给链队列添加一个头结点,并设定头指针指向头结点。因此,空队列的判定条件就变为头指针和尾指针都指向头结点。图3.8给出了链队列头尾指针与数据元素的关系。图3.8(a)是具有一个头结点的空队列; 元素a、b相继入队,将链队列中最后一个结点的指针域指向新元素,还要将队列中的尾指针指向新元素,如图3.8(b)和图3.8(c)所示; 在图3.8(c)的情况下,删除队首元素a,只需修改头结点的指针域,将其指向队首元素的下一个结点,如图3.8(d)所示; 若链队列中只有一个元素时,除了修改头结点的指针域,还要修改链队列的尾指针,将其指向头结点,如图3.8(e)所示。



图3.8链队列指针front、rear与数据元素的关系


链队列的基本运算如下。

1. 初始化队列

void Init_Queue(LINKQUEUE *q)

{/*创建一个带头结点的空链队列q*/

Linknode *p;

p= (Linknode*)malloc(sizeof(Linknode));/*申请链队列头结点*/

p->next=NULL;

q->front=q->rear=p;

}
2. 判队列空操作

intEmpty_Queue(LINKQUEUE *q)

{ /*队列q为空时,返回1; 非空时,返回0*/

if (q->front==q->rear) return 1;

elsereturn 0;

}
3. 入队列操作

void Add_Queue(LINKQUEUE *q,datatype x)

{/*将元素x插入到链队列q中,作为q的新队尾*/

Linknode *p;

p=(Linknode*)malloc(sizeof(Linknode));

p->data=x;

p->next=NULL;/*置新结点的指针为空*/

q->rear->next=p;/*将链队列中最后一个结点的指针指向新结点*/

q->rear=p;/*将队尾指向新结点*/

}
4. 出队列操作

datatype Del_Queue(LINKQUEUE *q)

{ /*若链队列q不为空,则删除队头元素,
并由x返回其元素值*/

Linknode *p;

datatype x;

if (Empty_Queue(q))/*队列为空*/

{ printf(" Queue empty\n"); 

return NULL;}

else

{p=q->front->next;/*取队头*/

x=p->data;

q->front->next=p->next;/*删除队头结点*/

if(p->next==NULL)

q->rear=q->front;

free(p);

return x;

}

}
5. 读队头元素操作

datatype Front_Queue(LINKQUEUE *q)

{ /*若队列q不为空,则返回队头元素*/

Linknode *p;

datatype x;

if (Empty_Queue(q))/*队列为空*/

{ printf(" Queue empty\n"); 

return NULL;}

else { p=q->front->next;/*取队头*/

x=p->data;

return x;}

}
3.4队列的应用

【例3.3】编写程序,从键盘输入一个整数序列a1,a2,…,an,若ai>0,则ai入队列; 若ai<0,则队头元素出队列; 若ai=0,则算法结束。要求利用循环队列完成,并在出现异常(如队空)时打印错误信息。

算法思路: 先建立一个空的循环队列Q,然后通过循环接收用户输入的整数。若输入的值大于0,则将该数入队列; 若输入的值小于0,则将一个元素出队列并输出; 若输入的值等于0,则退出循环。

void Fun(SEQUEUE *q)

{

int x;

Init_Queue(q);

scanf("%d",&x);

while(x!=0)

if(x>0)

{

if(!Add_Queue(q,x))

{printf("队列满!\n");break;}

elseif (!Del_Queue(q))

{printf("队列空!\n");break;}

scanf("%d:,&x);

}

}

本 章 小 结

(1) 栈是一种限定其插入、删除操作只能在线性表的一端进行的特殊结构。由于在栈中的数据元素具有后进先出的特点,因此,人们又将它称为LIFO线性表。栈可以用顺序存储结构和链式存储结构表示。

(2) 队列是一种限定其插入在线性表的一端进行,删除则在线性表的另一端进行的特殊结构。由于在队列中的数据元素具有先进先出的特点,因此,人们又将它称为FIFO线性表。同栈一样,队列也可以利用顺序存储结构或链式存储结构表示。在利用顺序存储结构表示时,为了避免“假溢出”现象的发生,需要将队列构成循环状,这就形成了循环队列。

(3) 分别介绍了栈和队列的各种运算的算法实现及简单应用。

知 识 拓 展

栈这种处理事情的先后机制在生活中并不多见,因为它“不合情理”。人们通常认为凡事先来后到、先来先处理是公平的,先来反而等待很长时间是不公平的,因此很多人刚进入计算机专业时,会奇怪为什么实行这样一种机制。但是在计算机处理问题时,常常要把一个大问题自上而下分解成很多小问题,一个个拆解开、分别解决之后,再回过头来得到整个问题的答案。栈能够记录这中间一步步分解的复杂过程,而且由于最后合并的过程与开始时拆解的过程正好相反,因此它后进先出的特点可以很自然地将已分解的问题合并。为了体会这一点,不妨思考一下通过堆栈实现二叉树深度优先遍历的过程。从事计算机软件开发的人,要想“站得高,看得远”,就要对自己提出更高的要求,对于自己所经手的任何程序,必须了解它内部各种函数和过程调用的关系,而这些调用都是在栈的帮助下实现的。因此,对栈的理解可以帮助开发者自上而下深入理解一个程序。

计算机科学中所说的队列和日常生活中相应的概念是一致的。例如,要对一段视频进行处理,需要把它变成一帧帧图像,然后按照时间顺序送入程序中,按照先来后到的顺序逐一处理,处理后再合并、拼接成完整的视频。由于在视频文件中后一帧的内容和前一帧有相关性,因此先后顺序必须保持原样,不能随意变动,这就需要有一种先来先服务、先进先出的机制,也就是队列机制。