项目三栈和队列

Download Report

Transcript 项目三栈和队列

项目三 栈与队列
项目导读
栈和队列是两种特殊的线性结构,是线性表特例。一般,在
线性表上的插入、删除运算等操作不受限制,而栈和队列上
的插入、删除操作会受某种限制,对它们来说,插入和删除
运算均是对首尾两个元素进行的。
本章主要介绍栈和队列的逻辑特征及其在计算机中的存储表
示、栈和队列的基本运算的算法描述以及栈和队列的应用。
教学目标
通过本章学习,要求掌握以下内容:
1.栈的基本概念和栈的基本运算。
2.栈的应用。
3.队列的基本概念和队列的基本运算。
4.队列的应用。
3.1 栈
3.1.1 栈的定义
栈(Stack)是限定仅在表的一端进行插入或删除操作
的线性表。在表中允许进行插入或删除操作的一端称为栈项
(Top),而另一端称为栈底(Bottom)。不含元素的栈称
为空栈。栈的插入和删除操作分别称为进栈和出栈。进栈是
将一个数据元素存放栈顶,出栈是将栈顶元素取出。若给定
栈S=(a1,a2,…,an),如图3-1所示的栈中,a1是栈底
元素,an是栈顶元素。由于只允许在栈顶进行插入和删除,
所以栈的操作是按“后进先出”的原则进行的。因此栈又称
为后进先出表,即LIFO(Last In First Out)线性表。
图3-1线结构示意图
在日常生活中,有许多类似栈的例子,如刷洗盘子时,
把洗净的盘子一个接一个地向上放(相当于进栈),取盘子
时,则从上面一个接一个地向下拿(相当于出栈)。
栈的基本运算有五种:
1.初始化栈:将栈置为空栈,不含任何元素,只建立
起栈顶指针。
2.进栈:向栈顶插入一个新的元素。
3.出栈:删除栈顶元素。
4.判栈空:判断一个栈是否为空栈。
3.1.2 栈的顺序存储及其基本操作的实现
栈的顺序存储结构,简称顺序栈。它是利用一组地址连
续的存储单元依次存放自栈底到栈顶的数据元素。与顺序表
的数据类型描述类似,栈的C语言描述如下:
#define MAX 50
/*定义MAX为栈的最大容量,例如设为
10*/
int s[MAX];
/*定义数组s,用来存储栈的元素,以整数
为例*/
int top;
/*定义栈顶指针为top*/
在这个描述中,假设栈中数据元素的类型是整型,数组
s存放栈中数据元素,数组最大容量为MAX,栈顶指针为top。
由于栈顶的位置经常变动,所以要设一个栈顶指针top。
栈顶指针top指向下一次进栈的数据元素存放位置。当栈中
没有数据元素时,栈是空栈,这时top=0。当栈中放满元素
时,top=MAX,表示栈满。若再有数据元素进栈,栈将溢出,
称为“上溢(overflow)。这是一个错误状态。反之,当
top=0时,再要进行出栈操作时,则发生“下溢
“(underflow)。在应用中通常作为控制程序转移的条件。
下面以一个例子来说明栈的操作。
设有一栈s=(a,b,c,d,e),则MAX为5,它的顺
序存储结构如图3-2所示。其中图3-2(a)是空栈状况;图
3-2(b)是进栈一个元素“a”之后的栈状况;图3-2(c)是
在图3-2(b)基础上连续将元素“b、c、d、e”进栈之后的
栈状况,此时是栈满状况,不允许有元素进栈;图3-2(d)
是在图3-2(c)基础上出栈一个元素后的栈状况。由此可见,
非空栈中的栈顶指针top始终指向栈顶元素的下一个位置。
图3-2 栈的顺序存储结构
顺序栈的基本运算有初始化栈、进栈、出栈和判栈空。
下面分别介绍栈的几种基本运算。
1.初始化栈
初始化栈算法描述如下:
void initstack(int top){
/*建立一个空栈s*/
top=0; /*设置栈顶指针top为0,表示栈空*/
}
2.进栈
进栈是在栈顶插入新的元素。由于进栈时可能会遇到栈
满,导致操作不能进行,此时进行溢出处理,返回函数值0;
如果栈未满,则栈顶指针加1,插入新元素,返回函数值1。
进栈算法描述如下:
#define MAX 10
int top;
/*定义栈顶指针为top*/
int push(int s[],int x)
{/*x为要插入的新元素*/
if(top==MAX)
{printf(″stack overflow″);
/*栈满信息*/
return( 0);
}
else
{
s [top]=x;
/*数据入栈*/
top=top+1;
/*当栈不满时,栈顶加1*/
printf(″ok″);
return (1);
}
}
main()
/*主程序*/
{
int a[MAX]={1,2,3,4,5};
int x=56,i;
top=5;
if(push(a,x))
for(i=0;i<top;i++)
printf(“%3d”,a[i]);
/*函数调用*/
}
例如栈为{1,2,3,4,5},想让元素“56”进栈,调用进
栈函数后,栈的内容成为{1,2,3,4,5,56}。
3.出栈
出栈是从栈顶删除元素。由于出栈时可能会遇到栈空,
此时进行下溢处理,返回函数值0;如果栈不空,则使栈顶指
针减1,然后将栈顶元素取出,返回函数值1。
出栈算法描述如下:
#define MAX 10
int top;
int pop(int s[],int *y)
{
if(top==0)
/*如果栈空*/
{
printf(″stack is empty″); /*输出栈空信息*/
return 0;
}
else
/*如果栈不空*/
{
top=top-1;
/*栈顶指针减1*/
*y=s[top];
printf(″ok″);
return 1; }
}
}
main()
/*主程序*/
{
int a[MAX]={1,2,3,4,5};
int y;
top=5;
if(pop(a,&y))
printf(“%d”,y); /*输出出栈元素*/
}
例如栈内容为{1,2,3,4,5},调用出栈函数后,栈
的内容成为{1,2,3,4}。
4.判栈空
如果栈空,则返回值1,如果栈不空,则返回值0。
判栈空算法描述如下:
void empty(int s[],int top)
{
if(top==0)
/*如果栈空*/
return(1);
/*返回值1*/
else
/*如果栈不空*/
return(0);
/*返回值0*/
}
对于进栈算法中的“上溢”应设法避免。避免出现“上
溢”的惟一方法是将栈的容量加到足够大。若一个程序使用
多个栈时,往往事先难以估讲每个栈所需容量,在实际应用
中一个栈发生“上溢”其他栈可能还留有很多空间,可以用
移动数据元素位置的方法加以调整。以达到多个栈共享存储
空间的目的。
现在,我们以两个栈为例讨论共享存储空间(一维数组
s[MAX],MAX是预先定义的容量)的方法。可以把两个栈
的栈底分别设在给定存储空间的两端,然后各自向中间伸展
如图3-3所示,仅当两个栈的栈顶相遇时才可能发生上溢,
这样两个栈之间可以做到互补余缺,使得某个栈实际可利用
的最大空间大于MAX/2。设第一个栈自顶向下伸展,第二个
栈自底向上伸展,t1指向第一个栈的栈顶元素的下一个位置,
而t2则指向第二个栈的栈顶元素的上一个位置,两个栈的初
态分别为t1=0,t2=MAX-1,上溢条件是t2+1=t1。
双栈的C语言描述如下:
#define MAX 50
/*定义MAX为栈的最大容量,例如设为
10*/
int s[MAX];
/*定义数组s,用来存储栈的元素,以整数为
例*/
int t1,t2;
/*定义两个栈顶指针分别为t1和t2*/
下面介绍双栈的进栈、出栈的操作。
进栈
向第i个栈插入元素x,若插入成功函数返回值为1,否则函数返
回值为0,算法如下:
#define MAX 50
int s[MAX];
int t1,t2;
int pushd ( int s[],int i, int x)
{/*元素x进第i个栈(i=1,2)*/
if (i<1|| i>2)
return(0);
if (t2+1==t1)
return(0);
if(i==1)
{s[t1]=x;
t1++;
}
else
{s[t2]=x;
t2--;
}
return(1);}
main()
{int i,j,x,p;
for(i=0;i<10;i++)/*第一栈输入数据*/
scanf(“%d”,&s[i]);
t1=10;
for(j=49;j>40;j--)/*第二栈输入数据*/
scanf(“%d”,&s[j]);
t2=40;
scanf(“%d,%d”,&p,&x); /*输入往第p个栈插入数据x*/
if (pushd ( s,p,x))
{ for (i=0;i<t1;i++)
printf(“%5d”,s[i]);
for (j=49;j>t2;j--)
printf(“%5d”,s[j]);
}
}
(2)出栈
第i个栈出栈算法。若第i(i=1,2)个栈为空,则函数返回值为0,否则函数
返回值为1。
#define MAX 50
int s[MAX];
int t1,t2;
int popd ( int s[], int i)
{/*第i个栈退栈*/
int y;
if (i<1|| i>2)
return(0);
if (i==1)
if(t1==0)
return(0);
else
{ t1--;
y=s[t1];printf(“%d”,y);}
else
if(t2==MAX-1)
return(0);
else
{t2++;y=s[t2];printf(“%d”,y);}
return(1);
}
main()
{int i,j,x;
for(i=0;i<10;i++)/*第一栈输入数据*/
scanf(“%d”,&s[i]);
t1=10;
for(j=49;j>40;j--)/*第二栈输入数据*/
scanf(“%d”,&s[j]);
t2=40;
scanf(“%d”,&x); /*输入第x个栈要进行出栈操作*/
if (popd ( s,x))
{ for (i=0;i<t1;i++)
printf(“%5d”,s[i]);
for (j=49;j>t2;j--)
printf(“%5d”,s[j]);
}
}
3.1.3 栈的链式存储及其基本操作的实现
当栈的最大容量事先不能估计时,也可采用链表作存储
结构,简称为链栈,其结点类型定义为:
typedef struct node
{
int data;
/*这里以整型为例*/
struct node*next;
/*指针类型,存放下一个结点
的地址*/
}NODE;
NODE *top;
/*链栈表头指针,即栈顶指针*/… ^
设top是指向栈顶结点的指针,其初值为空,说明在初始
状态下,栈中没有任何结点。如图3-4(a)所示,当把一个
数据元素x入栈时,先向系统申请一个结点p,置其数据域值
为x,把top结点作为p结点的直接后继,top改指到p结点。
如图3-4(b)所示。出栈时,先取出栈顶结点中的数据,然
后把该结点链域的值赋给栈顶指针top,释放该结点。如图34(c)所示。
图3-4栈的插入和删除运算
链栈的主要运算有进栈和出栈。
1.进栈
当向链栈插入一个新元素时,首先要向系统申请一个结
点的存储空间,将新元素的值写入新结点的数据域中,然后
修改栈顶指针。设要插入的新元素为x,以整型为例。进栈
的算法描述如下:
#include <stdio.h>
#include <stdlib.h>
typedef struct node
{
int data;
/*这里以整型数据为例*/
struct node*next;
/*指针类型,存放下一个结点地
址*/
}NODE;
NODE *crea_linkstack()
/*建立链栈*/
{
NODE*top,*g;
/*定义栈顶指针*/
char j;
int a;
top=NULL;
j=getchar();
while(j!=′?′)
/*判断生成链栈结束标志*/
{
scanf(″%d″,&a);
g=(NODE*)malloc(sizeof(NODE));
g->data=a;
g->next=top;
top=g;
j=getchar();
}
return(top);
/*返回栈顶指针*/
}
NODE * pushstack(NODE*top,int x)
{
NODE*p;
/*定义结点p*/
p=(NODE*)malloc(sizeof(NODE));
p->data=x;
/*将要插入的数据x存储到结点p的数据
域中*/
p->next=top;
/*将p结点的指针修改为原top的指针*/
top=p;
/*将top修改为p*/
return(top);
}
main()
/*主程序*/
{
int y=34;
/*将入栈的元素*/
NODE*top,*p; top=crea_linkstack();
/*建立链栈*/
top=pushstack(top,y);
/*入栈*/
p=top;
/*输出整个链栈,print为第二章编写的输出链表函
数*/
while (p!=NULL)
{printf(“%5d”,.p->data);
p=p->next;
}
}
例如调用建立链栈函数后所建立的链栈数据元素为1,2,3,
4,5,6,将元素34入栈,调用入栈函数后显示为34,6,5,
4,3,2,1。
2.出栈
当出栈时,先取出栈顶元素的值,再修改栈顶指针,释放原
栈顶结点。出栈的算法描述如下:
#include<stdio.h>
typedef struct node {
int data;
struct node*next;}NODE;
NODE*popstack(NODE*top,int*q)
{NODE*p; /*定义q结点*/
if(top!=NULL) /*如果栈不空*/
{p=top; /*p指向栈顶结点*/
*q=top->data;/*将要读出的数据放入*q中*/
top=top->next; /*修改top指针*/
free(p);
}
return(top); /*返回栈顶指针*/3栈和队列
}
main() /*主程序*/
{ int b;
NODE*top;
top=crea_linkstack();/ 建立链栈的函数见上面*/
top=popstack(top,&b);
printf(“%5d”,b);
}
例如已知链栈数据元素为1,2,3,4,5,6,7,调用出栈
函数后的显示结果为7。
3.1.4 栈的应用举例
栈是计算机软件中应用最广泛的数据结构之一。比如,在编
译系统中的表达式求值,程序递归的实现,子程序的调用和返回
地址的处理等。下面介绍栈的几个应用实例。
1.算术表达式的计算
表达式求值是编译系统中的一个基本问题,目的是把人们平
时书写的算术表达式变成计算机能够理解并能正确求值的表达方
法。
算术表达式中包含算术运算符和算术量,在各运算符之间存
在着优先级,运算时必须按优先级顺序进行运算,先运算高级别
的,后运算低级别的,而不能简单地从左到右进行运算。因此,
进行表达式运算时,必须设置两个栈,一个栈用于存放运算符,
另一个栈用于存放操作数。在表达式运算过程中,编译程序从左
到右进行扫描,遇到操作数,就把操作数放入操作数栈,遇到运
算符时,要把该运算符与运算符栈的栈顶比较。如果该运算符优
先级高于栈顶运算符的优先级,就把该运算符进栈,否则退栈。
退栈后,在操作数栈中退出两个元素,其中先退出的元素在运算
符右,后退出的元素在运算符左,然后用运算符栈中退出的栈顶
元素(运算符)进行运算,运算的结果存入操作数栈中。反复进
行上述操作,直到扫描结束。此时,运算符栈为空,操作数栈只
有一个元素,即为最终的运算结果。
例3.1 栈求表达式6-8/4+3*5的值。栈的变化见表3-1。
步骤
操作数栈
运算符栈
说明
开始
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
6
6
68
68
684
6
62
4
4
43
43
435
4
4 15
19
19
-/
-/
+
+
+*
+*
+
+
开始时两个栈为空
扫描到“6”,进入操作数栈
扫描到“-”,进入运算符栈
扫描到“8”,进入操作数栈
扫描到“/”,进入运算符栈
扫描到“4”,进入操作数栈
扫描到“+”,“/”“8”、“4”退栈
计算8/4=2进操作数栈
扫描到“+”,“-”“6”、“2”退栈
6-2=4进操作数栈
扫描到“+”,进入运算符栈
扫描到“3”,进入操作数栈
扫描到“*”,进入运算符栈
扫描到“5”,进入操作数栈
扫描完,“*”“3”、“5”退栈
3*5=15进操作数栈
“+”“4”、“15”退栈
4+15=19进操作数栈
表达式的值为19
表3-1 用栈求表达式的值
2.函数递归的实现
递归是程序设计中常用的方法之一。栈的一个重要的应用是
可以用来实现递归函数(或递归过程)。
例3.2 通常用来说明递归的最简单的例子是阶乘的定义,它
可以表示成:
1
(n=0,1)
n>1
n(n-1)!
n!=
由定义可知,为了定义n的阶乘,必须首先定义(n-1)的
阶乘,为了定义(n-1)的阶乘,又必须定义(n-2)的阶乘……这
种用自身的简单情况来定义自己的方式,称为“递归定义”。
一个递归定义必须一步比一步简单,最后是有终结的,决不
能无限循环下去。在n的阶乘的定义中,当n为0或1时定义为
1,它不再用递归来定义。根据阶乘的定义,我们编写的C语
言程序如下:
int fac(int n)
{/*计算n! */
if(n==0||n==1) return(1);
else return(n*fac(n-1));
}
main()
{int n,y;
scanf(“%d”,&n);
y=fac(n);
printf(“%d!=%d”,n,y);
}
函数据直接或间接调用自身称为函数的递归调用。包含
递归调用的函数称为递归函数。像函数fac(n)中含直接调用
函数fac,这就称为直接递归调用。若函数a中调用函数b,
而函数b中又调用了函数a,这就称为间接递归调用。
实现递归调用的关键是建立一个栈。这个栈是由系统提
供的运行工作栈。计算机在执行递归算法时,是通过栈来实
现的。在一层层递归调用时,系统自动将其返回地址和处在
每一调用层的变量数据一一记下并进栈。返回时,它们一一
出栈并且被采用。图3-5展示了递归调用中的执行情况。从
图3-5可以看到fac函数共被调用4次,即fac(4)、fac(3)、
fac(2) 、fac(1)。其中fac(4)是在main函数据中调用的,其余
3次是在fac函数中调用的。在某一层递归调用时,并未立即
得到结果,而是进一步向深度递归调用,直到最内层函数执
行n=1时,fac(n)才有结果。然后在一一返回时,不断得到中
间结果,直到回到主程序得到n!的最终结果为止。
图3-5递归调用示意图
实际上,递归是把一个不能或不好直接求解的“大问题”
转化成一个或几个“小问题”来解决,再把这些“小问题”
进一步分解成更小的“小问题”来解决,如此分解,直至每
个“小问题”都可以直接解决(此时分解到递归出口)。当
然,被分解的“小问题”和“大问题”必须具有相同的特征
属性。
虽然递归算法简明精练,但运行效率较低,时
空开销较大,并且某些高级语言没有提代递归调用
的语句及功能。因此,在实际应用中往往会使用非
递归方法。为了提高程序设计的能力,有必要进行
由递归方法到非递归方法的基本训练。通过前面对
递归算法执行的分析,我们已经知道,系统内部是
借助于栈来实现递归的。因此,在设计相应的非递
归算法时也需要人为地设置一个栈来实现。具体如
何实现将在后面的有关章节中详细介绍
3.2 队列
3.2.1 队列的定义
队列(queue)是限定只能在表的一端进行插入,在表
的另一端进行删除的线性表。表中允许插入的一端称为队尾
(rear),允许删除的一端称为队首(front)。
假设队列为Q=(a1,a2,a3,…,an),那么a1就是
队首元素,an则是队尾元素。队列中的元素是按照a1,a2,
a3,…,an的顺序进入的,出队列也只能按照这个次序依次
退出,图3-6是队列的示意图,因此,队列又称为先进先出
表(First In First Out,简称FIFO)。如果队列中没有任何
元素,则称为空队列,否则称为非空队列。
图3-6队列的示意图
在日常生活中,有许多队列的例子,如顾客排队购物,
排在队伍前面的顾客先买,先离队。排在队伍后面的顾客后
买,后离队。队列的基本运算有五种:
1.初始化队列:将一个队列设置为空队列。
2.入队列:在队尾插入一个新的元素,也称进队或插入。
3.出队列:在队首删除一个元素,也称退队或删除。
4.取队首元素:得到队首元素的值。
5.判队空:判断队列是否为空队列。
在日常生活中,有许多队列的例子,如顾客排队购物,
排在队伍前面的顾客先买,先离队。排在队伍后面的顾客后
买,后离队。队列的基本运算有五种:
1.初始化队列:将一个队列设置为空队列。
2.入队列:在队尾插入一个新的元素,也称进队或插入。
3.出队列:在队首删除一个元素,也称退队或删除。
4.取队首元素:得到队首元素的值。
5.判队空:判断队列是否为空队列。
3.2.2 队列的顺序存储及其基本操作的实现
队列的顺序存储结构称为顺序队列(sequential
queue)。顺序队列与顺序表一样,用一个一维数组来存放
数据元素。在内存中,用一组连续的存储单元顺序存放队列
中各元素。队列的C语言描述如下:
#define MAX 10 /*定义MAX为队列最大容量,例如设为
10*/
int q[MAX]; /*定义数组q,用来存放队列的元素,以整
数为例*/
int front=-1,rear=-1; /*定义队首和队尾指针,并赋初值*/
在这个描述中,用一维数组q[MAX]表示队列,其中
MAX表示队列允许的最大容量,用一个指针front指示队列的
首端,用另一个指针rear来指示队列的尾端。为方便起见,
约定头指针front总是指向队首的前一个位置,尾指针rear指
向队尾的所在位置。
在队列顺序存储结构中如图3-7所示,队首指针和队尾指
针是数据元素的下标。在队列为空的初始状态front=rear=-1。
每当向队列中插入一个元素,尾指针rear向后移动一位,
rear=rear+1。当rear=MAX-1时,表示队满。每当从队列中
删除一个元素时,队首指针也向后移动一位,front=front+1。
经过多次进队和出队运算,可能出现front=rear的时候,这
时队列为空。
图3-7表示队列出、入队操作示例。
下面给出顺序队列中实现进队与出队运算算法描述。
1.进队
在顺序队列中,进队时,数据元素是从队尾进入的,应
该先改变队尾指针,使队尾指针指向新元素应插入的位置,
即队尾位置,然后将数据插入。由于顺序队列有上界,因此
会发生队满的情况。在插入时,若发生队满,应返回队满信
息。
#define MAX 10
int q[MAX]={1,2,3,4,5};
int front=-1,rear=-1;
int inqueue(int x)/*插入元素为x*/
{ if (rear==MAX-1) /*如果队满*/
{printf(“overflow\n”);/*打印溢出*/
return(rear);}
else /*如果队不满*/
{q[++rear]=x; /*队尾指针加1*/
return(rear);/*插入成功返回队尾指针*/
}
}
main() /*主程序*/
{ int i,h=6; /*将h入队*/
rear=4;
rear=inqueue(h);
for(i=front+1;i<=rear;i++)
printf(″%d″,q[i]); /*显示整个队的内容*/
}
例如已知队列为{1,2,3,4,5},将元素“6”入队,
调用入队函数后显示结果为{1,2,3,4,5,6}。
2.出队
出队时将队首指针所指元素取出,但在队列结构定义时,
队首指针指向队首元素的前一个位置,因此,应先使队首指
针加1,然后取出队首指针所指元素。当队列空时,函数返
回队空信息0。
#define MAX 10
int q[MAX]={1,2,3,4,5};
int front=-1,rear=-1;
int delqueue(int *y) /*出队,出队的值放在y中*/
{if(front==rear) /*如果队空*/
{ printf(“queue empty”)
return(front);}
else /*如果队不空*/
{*y=q[++front]; /*头指针加1,队首结点送入指针y所指
向的变量中*/
return(front);
}
}
main() /*主程序*/
{ int i,x;
rear=4;
front=delqueue(&x);
for(i=front+1;i<=rear;i++)
printf(″%d″,q[i]); /*显示整个队列*/
}
例如已知队列为{1,2,3,4,5},则调用出队函数
后显示的结果为{2,3,4,5}。
若存储队列的一维数组中所有位置上都有元素,即尾指
针指向一维数组最后,而头指针指向一维数组开头,则队满。
不能再向队列中插入元素。
但可能会出现这样情况,尾指针指向一维数组最后,但
前面有元素已经出队,这时要插入元素,仍然发生溢出,而
实际上队列并未满。这种溢出称为“假溢出”,如图3-7
(C)。克服“假溢出”的方法有两种:一种是将队列中的
所有元素均向最前端位置移动,显然这种方法是很浪费时间
的;另一种方法是采用循环队列。
循环队列是将存储队列的存储区看成是一个首尾相连的
环,即将表示队列的数组元素q[0]与q[MAX-1]连接起
来,形成一个环形表。如图3-8所示。
在循环队列中,容量设为MAX,队首指针为
front,队尾指针为rear。初始状态为front=rear=0。
循环队列空标志为rear=front,循环队列满的标志为
(rear+1)%MAX=front,在循环队列满时,队列
中实际上还有一个空闲单元,以防止空队与满队的
标志发生冲突。当然为,也可以另设标志来区分队
满和队空,但运算时就要多花费一些时间。图3-9给
出了循环队列的进队、出队与指针的关系。
图3-9 循环队列的进队、出队示意图
下面给出循环队列中实现进队与出队运算算法的描述。
循环队列存储在数组cq[MAX]中,队尾指针为rear,队首指针为front。
1.进队
将一个新元素x进队,从队尾插入,首先要判断是否队满,若队满,则返
回队满信息。若队不满,则插入新元素,返回插入成功信息。
#define MAX 10
int q[MAX]={0,1,2,3,4,5};
int front=0,rear=0;
int incq(int x)
{
if((rear+1)%MAX==front) /*如果队满*/
return(0);
else /*如果队不满*/
{rear=(rear+1)%MAX; /*队尾指针加1*/
q[rear]=x; /*将新元素x插入到队尾*/
return(1);
}
}
main() /*主程序*/
{
int i,x=23;
rear=5;
if(incq(x))
for(i=front+1;i<=rear;i++)
printf(″%3d″,q[i]);/*显示整个队的内容*/}
2.出队
出队时要判断是否队空,若队为空,则返回队空信息0。
若队不为空,则将队首元素存入x中,返回出队成功信息1。
#define MAX 10
int q[MAX]={0,1,2,3,4,5};
int front=0,rear=0;
void delcq()
{
if(rear==front)/*如果队空*/
printf(“empty”);
else /*如果队不空*/
{front=(front+1)%MAX; /*队首指针加1*/
}
main() /*主程序*/
{ int rear=5;
for(i=front+1;i<=rear;i++)
printf(“%5d”,q[i]);
}
3.2.3 队列的链式存储及其基本操作的实现
顺序分配的循环队列虽然设计的很巧妙,但若有多个队
列共享内存时,比多个栈共享内存空间更为复杂。而链式分
配的队列可以很容易地实现多个队列的共享内存空间。
当队列采用链表作为存储结构时,被称作链队。其逻辑
状态如图3-10所示。
图3-10 带头结点的链式队列
链队列结点类型用C语言描述如下:
typedef struct node
{int data;
/*以整型为例*/
struct node*next;}NODE;
在链队列中,有一个头指针front和一个尾指针rear。与
单链表类似,另外给链队列增加一个附加表头结点。队列头
指针指向队列表头结点,队列尾指针指向队列的尾结点。队
列空的条件是front=rear,即头尾指针都指向表头结点。链
队列的入队运算在队尾进行,链队列的出队运算在队首进行。
1.入队
将x加入到链队列的尾部,并返回rear。算法描述如下:
#include<stdio.h>
#include <stdlib.h>
typedef struct node {
int data;
struct node*next;}NODE;
NODE *front,*rear;
NODE *crea_linkq()
{ NODE *s;
int x,tag;
printf(“enter flag”);
scanf(“%d”,&tag);
front=(NODE *)malloc(sizeof(NODE));
front->data=tag;
rear=front;
printf(“enter data x:”);
scanf(“%d”,&x);
while(x!=tag)
{ s=(NODE *) malloc (sizeof (NODE))
s->data=x;
rear->next=s;
rear=s;
scanf(“%d”,&x);
}
rear->next=NULL;
return(front);
}
NODE*pushq(int x)
{
NODE *p;
p=(NODE*)malloc(sizeof(node)); /*向系统申请一
存储单元p*/
p->data=x; /*将要插入的元素x放入p数据域*/
p->next=NULL; /*将p结点的指针域置为NULL*/
rear->next=p; .*原尾结点的指针指向新结点p*.
rear=p; /*新元素入队后的尾结点*/
return(rear);/*返回尾结点*/
}
main() /*主程序*/
{
NODE *p;
int x=34; /*x为要入队的元素*/
front=crea_linkq(); /*调用建立链队的函数,并将front指向队
首,rear指向队尾*/
rear=pushq(x); /*调用入队函数*/
p=front->next; /*显示整个队列*/
while (p!=NULL)
{printf(“%d”,p->data);
p=p->next;}
}
2.出队
从带表头结点的链队列中删除队首元素,并存入*p中,并将
存储单元释放,若队列为空返回空值,返回front。
#include<stdio.h>
#include <stdlib.h>
typedef struct node
{int data;
struct node*next;}NODE;
NODE *front,*rear;
NODE*popq(NODE*front,NODE*rear)
{int y ;
NODE*p;
if(front==rear) /*判断队空*/
return(front);
else /*队不空*/
{p=front->next; /*将头元素next指针放在p中*/
front->next=p->next;
if(p->next==NULL) /*在只有一个元素的情况下*/
rear=front;/*出队后,队空*/
y=p->data ;
free(p);
return(front); /*返回front*/
}
}
main() /*主程序*/
{
NODE *p;
front=crea_linkq(); /*调用建立链队的函数,见前述*/
front=popq(front,rear);
p=front->next; /*显示整个队列*/
while (p!=NULL)
{printf(“%d”,p->data) ;
p=p->next ;}
}
3.2.4 队列的应用举例
队列在日常生活中和计算机程序设计中应用非常广泛。
下面举两个计算机应用方面的例子。
例3.3 打印数据缓冲区问题。
在打印机打印的时候,数据是由主机传送给打印机的。
主机输出数据的速度比打印机打印的速度要快得多。若直接
把输出的数据送给打印机,由于速度不匹配,主机就要等待
打印机的打印工作,而不能进行其他工作。这样就大大影响
了主机的工作效率。
为了解决这个问题,通常是在内存中设置一个打印数据缓冲
区。缓冲区是一块连续的存储空间,把它设计成循环队列结
构,主机把要打印的数据依次写入到这个缓冲区中,写满后
就暂停输出,主机此时可以进行其他工作。打印机就从缓冲
区按照先进先出的原则依次取出数据并打印。打印完这批数
据后,再向主机发出请求,主机接到请求后,再向缓冲区写
入打印数据。
利用缓冲区,解决了计算机数据处理与打印机之间速度
不匹配的问题,从而提高了计算机的工作效率。
例3.4 键盘输入循环缓冲区问题。
键盘输入是另一个循环队列在计算机操作系统中应用的
实例。例如,当程序正在执行某一任务时,用户仍然可以从
键盘输入其他内容。用户输入的内容暂时未能在屏幕上显示
出来,当程序的当前任务结束时,用户输入的内容才显示出
来。
在这个过程当中,系统是将检测到的键盘输入的字符先存储
到一个缓冲区中,当系统当前任务结束后,就从键盘缓冲区
依次取出已输入的字符,并按要求进行处理。这是系统设置
的一个键盘缓冲区,也采用了循环队列方式。利用循环队列
的工作方式,对字符按次序处理。循环结构又可以有效地限
制了缓冲区的大小,有效地利用了存储空间。
习题3
一、 选择题
1. 对于栈操作数据的原则是( )。
A. 先进先出 B. 后进先出 C. 后进后出 D. 不分顺序
2. 在作进栈运算时,应先判别栈是否( ① ),在作退栈运算时应先判别栈是否
( ② )。当栈中元素为n个,作进栈运算时发生上溢,则说明该栈的最大容
量为( ③ )。
为了增加内存空间的利用率和减少溢出的可能性,由两个栈共享一片连续的
内存空间时,应将两栈的 ( ④ )分别设在这片内存空间的两端,这样,当
( ⑤ )时,才产生上溢。
①, ②: A. 空
B. 满
C. 上溢
D. 下溢
③: A. n-1
B. n
C. n+1
D. n/2
④: A. 长度
B. 深度
C. 栈顶
D. 栈底
⑤: A. 两个栈的栈顶同时到达栈空间的中心点.
B. 其中一个栈的栈顶到达栈空间的中心点.
C. 两个栈的栈顶在栈空间的某一位置相遇.
D. 两个栈均不空,且一个栈的栈顶到达另一个栈的栈底.
3.3 项目小结
1.栈是一种运算受到限制的特殊线性表,它仅允许在
线性表同一端进行插入和删除操作,栈是一种后进先出的线
性表,简称为LIFO表。
2.栈在日常生活和计算机程序设计中有着广泛的应用,
如算术表达式求值、函数的嵌套和递归调用等。
3.队列也是一种运算受到限制的特殊线性表,它仅允
许在线性表一端进行插入,在另一端进行删除,队列是一种
先进先出的特殊线性表,简称为FIFO表。
4.队列的链式存储结构与单链表类似,但删除结点只
能在表头,插入元素只能在表尾。
3. 一个栈的输入序列为123…n,若输出序列的第一个元素是n,
输出第i(1<=i<=n)个元素是( )。
A. 不确定
B. n-i+1
C. i
D. n-i
4. 若一个栈的输入序列为1,2,3,…,n,输出序列的第一个元素
是i,则第j个输出元素是( )。
A. i-j-1
B. i-j
C. j-i+1
D. 不确定的
5. 若已知一个栈的入栈序列是1,2,3,…,n,其输出序列为
p1,p2,p3,…,pN,若pN是n,则pi是( )。
A. i
B. n-i
C. n-i+1
D. 不确定
6. 有六个元素6,5,4,3,2,1 的顺序进栈,问下列哪一个
不是合法的出栈序列?( )
A. 5 4 3 6 1 2 B. 4 5 3 1 2 6 C. 3 4 6 5 2 1 D. 2 3 4 1 5
6
7. 设栈的输入序列是1,2,3,4,则( )不可能是其出栈序列。
A. 1,2,4,3,
B. 2,1,3,4,
C. 1,4,3,2,
D. 4,3,1,2,
E. 3,2,1,4,
8. 一个栈的输入序列为1 2 3 4 5,则下列序列中不可能是栈的
输出序列的是( )。
A. 2 3 4 1 5 B. 5 4 1 3 2 C. 2 3 1 4 5
D. 1 5 4 3 2
9. 设一个栈的输入序列是 1,2,3,4,5,则下列序列中,是
栈的合法输出序列的是( )。
A. 5 1 2 3 4
B. 4 5 1 3 2
C. 4 3 1 2 5
D. 3 2 1 5 4
10. 某堆栈的输入序列为a, b,c ,d,下面的四个序列中,不可
能是它的输出序列的是( )。
A. a,c,b,d
B. b, c,d,a C. c, d,b, a
D. d,
c,a,b
11. 设abcdef以所给的次序进栈,若在进栈操作时,允许退栈
操作,则下面得不到的序列为( )。
A.fedcba
B. bcafed
C. dcefba
D. cabdef
12. 设有三个元素X,Y,Z顺序进栈(进的过程中允许出栈),
下列得不到的出栈排列是( )。
A.XYZ
B. YZX
C. ZXY
D. ZYX
13. 用链接方式存储的队列,在进行删除运算时( )。
A. 仅修改头指针 B. 仅修改尾指针 C. 头、尾指针都要修改
D. 头、尾指针可能都要修改
15. 用不带头结点的单链表存储队列时,其队头指针指向队头结
点,其队尾指针指向队尾结点,则在进行删除操作时( )。
A.仅修改队头指针
B. 仅修改队尾指针
C. 队头、队尾指针都要修改 D. 队头,队尾指针都可能要修改
16. 递归过程或函数调用时,处理参数及返回地址,要用一种
称为( )的数据结构。
A.队列
B.多维数组
C.栈
D. 线性表
二、 判断题
1. 消除递归不一定需要使用栈,此说法( )
2. 栈是实现过程和函数等子程序所必需的结构。( )
3. 两个栈共用静态存储空间,对头使用也存在空间溢出问题。
( )
4.两个栈共享一片连续内存空间时,为提高内存利用率,减
少溢出机会,应把两个栈的栈底分别设在这片内存空间的两
端。( )
5. 即使对不含相同元素的同一输入序列进行两组不同的合法的
入栈和出栈组合操作,所得的输出序列也一定相同。( )
6. 栈与队列是一种特殊操作的线性表。( )
7. 若输入序列为1,2,3,4,5,6,则通过一个栈可以输出序列
3,2,5,6,4,1. ( )
8. 栈和队列都是限制存取点的线性结构。( )
9.若输入序列为1,2,3,4,5,6,则通过一个栈可以输出序列1,5,4,
6,2,3。( )
10. 任何一个递归过程都可以转换成非递归过程。(
)
11. 只有那种使用了局部变量的递归过程在转换成非递归过程
时才必须使用栈。(
)
12. 队列是一种插入与删除操作分别在表的两端进行的线性表,
是一种先进后出型结构。( )
13. 通常使用队列来处理函数或过程的调用。( )
14. 队列逻辑上是一个下端和上端既能增加又能减少的线性表。
( )
15. 循环队列通常用指针来实现队列的头尾相接。( )
16. 循环队列也存在空间溢出问题。( )
17. 队列和栈都是运算受限的线性表,只允许在表的两端进行
运算。( )
18. 栈和队列都是线性表,只是在插入和删除时受到了一些限
制。( )
19. 栈和队列的存储方式,既可以是顺序方式,又可以是链式
方式。( )
三、 填空题
1.栈是_______的线性表,其运算遵循_______的原则。
2._______是限定仅在表尾进行插入或删除操作的线性表。
3. 一个栈的输入序列是:1,2,3则不可能的栈输出序列是
_______。
4.两个栈共享空间时栈满的条件_______。
5.在作进栈运算时应先判别栈是否_(1)_;在作退栈运算时应先
判别栈是否_(2)_;当栈中元素为n个,作进栈运算时发生上
溢,则说明该栈的最大容量为_(3)_。为了增加内存空间的
利用率和减少溢出的可能性,由两个栈共享一片连续的空间
时,应将两栈的_(4)_分别设在内存空间的两端,这样只有
当_(5)_时才产生溢出。
6. 多个栈共存时,最好用_______作为存储结构。
7.用S表示入栈操作,X表示出栈操作,若元素入栈的顺序为
1234,为了得到1342出栈顺序,相应的S和X的操作串为
_______。
8. 循环队列的引入,目的是为了克服_______。
9.________又称作先进先出表。
10. 已知链队列的头尾指针分别是f和r,则将值x入队的操作序
列是_______。
11.区分循环队列的满与空,只有两种方法,它们是______和
______。
四、应用题
1. 名词解释:栈。
2. 名词解释:队列
3. 什么是循环队列?
4. 假设以S和X分别表示入栈和出栈操作,则对初态和终态均
为空的栈操作可由S和X组成的序列表示(如SXSX)。
(1)试指出判别给定序列是否合法的一般规则。
(2)两个不同合法序列(对同一输入序列)能否得到相同的
输出元素序列?如能得到,请举列说明。
5. 有5 个元素,其入栈次序为:A,B,C,D,E,在各种可能
的出栈次序中,以元素C,D最先出栈(即C第一个且D第二
个出栈)的次序有哪几个?
6. 如果输入序列为1 2 3 4 5 6,试问能否通过栈结构得到以下两
个序列:4 3 5 6 1 2和1 3 5 4 2 6;请说明为什么不能或如何才
能得到。
7. 若元素的进栈序列为:A、B、C、D、E,运用栈操作,能
否得到出栈序列B、C、A、E、D和D、B、A、C、E?为什
么?
8. 设输入序列为a,b,c,d,试写出借助一个栈可得到的两个输出序
列和两个不能得到的输出序列。
9. 设输入序列为2,3,4,5,6,利用一个栈能得到序列2,5,
3,4,6吗?栈可以用单链表实现吗?
五、 算法设计题
1. 设从键盘输入一整数的序列:a1, a2, a3,…,an,试
编写算法实现:用栈结构存储输入的整数,当ai≠-1时,将ai
进栈;当ai=-1时,输出栈顶整数并出栈。算法应对异常情况
(入栈满等)给出相应的信息。
2. 设表达式以字符形式已存入数组E[n]中,‘#’为表达
式的结束符,试写出判断表达式中括号(‘(’和‘)’)
是否配对的C语言描述算法:EXYX(E); (注:算法中可调用
栈操作的基本算法。)
3. 从键盘上输入一个逆波兰表达式,用伪码写出其求值
程序。规定:逆波兰表达式的长度不超过一行,以$符作为
输入结束,操作数之间用空格分隔,操作符只可能有+、-、*、
/四种运算。例如:234 34+2*$
项目实 训 2
实训目的要求:
1.掌握栈和队列的基本概念。
2.通过实验进一步加深理解栈的经过及其有关算法。
实训内容:
模拟停车场问题:
队列设有一个可以停放n辆汽车的狭长停车场,它只有一个大门
可以供车辆进出。车辆按到达停车场时间的先后次序从停车场最
里面向门口处停放(最先到达的第一辆车停在停车场的最里面)。如
果停车场已放满n辆车,则后来的车辆只能在停车场大门外的便道
上等待,一旦停车场内有车开走,则排在便道上的第一辆车就可进
入停车场。停车场内如有某辆车要开走,在它之后进入停车场的车
辆都必须先退出停车场为它让路,待其开出停车场后,这些车辆再依
原来的次序进入。每辆车在离开停车场时,根据它在停车场内停留
时间的长短交费。如果停在便道上的车辆未进停车场就要离去,
允许其离去时不收停车费,并且仍然保持在便道上等待的车辆的次
序。现在编制一个程序来模拟停车场的管理。
说明:
首先确定模拟程序中需要的数据结构及其操作。
由于停车场只有一个大门,因此可用一个栈来模拟;根
据便道停车的特点,先排队的车辆先离开便道进入停车场,可
以用一个队列来模拟;又因为排在停车场中间的车辆可以提
前离开,因此还需要有一个地方(车辆规避所)保存为了让路
离开停车场的车辆,很显然这也应该用一个栈来模拟。所以在
程序中设置了两个顺序栈s1和s2分别表示停车场和规避所;
设置了一个链队列q表示便道。它们的数据类型定义在下面
的源程序中,为了操作方便,链队列表头结点中的num域中
存放便道上的车辆数量。
程序执行时,当输入数据表示有车辆到达时,判断栈s1
是否满,若未满就将新数据进栈s1;若栈已满,就将数据入
队列q,表示车辆在便道上等待进入停车场。该操作过程由
函数Arrive完成。当输入数据表示有车辆要离去时,就在栈s1
中寻找此车牌号的车辆,如寻找到就让其离开停车场,并根据
停车时间计费,同时将队列q的队头元素进栈s1;如没有找
到,就到队列q中去寻找此车牌号的车辆。如在队列q中找到
就允许其离开队列,并不收费;如找不到就显示出错信息。当
离开停车场的车辆位于栈s1的中间时,必须先将此位置到栈
顶之间的所有数据倒到栈s2中去,然后安排车辆出栈s1,最后
将栈s2中的数据倒回到栈s1中来。该操作过程由函数Delive
完成。显然,以上两个主要操作需要利用栈和队列的两个基
本操作入栈(队列)和出栈(队列)来实现。源程序中的函
数Display则可以随时显示停车场的状况。
实训参考程序:
下面是模拟停车场管理的源程序。
#include "stdio.h"
#define N 2 /*停车场容量*/
#define M 5 /*停车单价*/
#define True 1
#define False 0
typedef struct{
int num;
/*车牌号*/
int arrtime; /*到达/离开时间*/
}ELEMTP;
/*顺序栈的数据元素类型*/
typedef struct{
ELEMTP elem[N];
int top;
}SqStack;
/*顺序栈类型*/
typedef struct node{
int num;
/*车牌号/便道上的车辆数量*/
struct node *next;
}QNode;
/*链队列的数据元素类型*/
typedef struct{
QNode *front, *rear;
}LQueue;
/*链队列类型*/
int Push_Sq(SqStack *s,ELEMTP x); /*入栈*/
ELEMTP Pop_Sq(SqStack *s);
/*出栈*/
void InitQueue_L(LQueue *q);
/*初始化队列*/
void EnQueue_L ( LQueue *q,int num1); /*入队列*/
int DelQueue_L(LQueue *q);
/*出队列*/
void Arrive (SqStack *s1, LQueue *q,ELEMTP x){
/*车辆x进入停车场*/
int f;
f=Push_Sq(s1,x);
if (f==False){
/*停车场栈s1已满入便道q */
EnQueue_L(q,x.num);
printf("第%d号车停在便道第%d车位上\n",x.num,q->front>num);
}
else printf("第%d号车停在停车场第%d车位上
\n",x.num,s1->top);
}/* Arrive */
void Delive (SqStack *s1,SqStack *s2, LQueue *q,ELEMTP
x){
/*车辆x离开停车场*/
int n,f=False;
ELEMTP y; QNode *p;
while ((s1->top>0) && (f!=True)){ /*在栈s1中寻找车辆x */
y=Pop_Sq(s1);
if (y.num!=x.num) n=Push_Sq(s2,y);
else f=True;
}
if (y.num==x.num){
/*寻找到车辆x*/
printf("第%d号车应收费%d元\n",y.num,(x.arrtimey.arrtime)*M);
while (s2->top>0){
/*将栈s2中的车辆倒回到栈s1中*/
y=Pop_Sq(s2);
f=Push_Sq(s1,y);
}
n=DelQueue_L(q);
if (n!=NULL){
/*便道q上的第一辆车入栈s1*/
y.num=n;
y.arrtime=x.arrtime;
f=Push_Sq(s1,y);
printf("第%d号车停在停车场第%d号车位上\n",y.num,s1>top);
}
}
else{
/*栈s1中未找到车辆x*/
while (s2->top>0){ /*将栈s2中的车辆倒回到栈s1中*/
y=Pop_Sq(s2);
f=Push_Sq(s1,y);
}
p=q->front;
/*在便道q上找到车辆x*/
f=False;
while (f==False && p->next!=NULL)
if (p->next->num!=x.num)
p=p->next;
else{
p->next=p->next->next;
q->front->num--;
if (p->next==NULL)
q->rear=q->front;
printf("第%d号车离开便道\n",x.num);
f=True;
}
if (f==False)
printf("输入数据错误,停车场和便道上均无%d号车\n",x.num);
}
}/* Delive */
void Display(SqStack *s1, LQueue *q){
/*显示停车场的状况*/
int k; QNode *p;
printf("停车场状况:\n");
if(s1->top!=0){
printf("车道 车号\n");
for(k=0;k<s1->top;k++)
printf("%d %d\n",k+1,s1->elem[k].num);
}
else printf("停车场没有车辆\n");
printf("便道状况:\n");
if(q->front->num){
printf("车道 车号\n");
for(k=1,p=q->front->next;p;p=p->next)
printf("%d %d\n",k++,p->num);
}
else printf("便道没有车辆\n");
}/* Display */
void main()
{ char ch1,ch2;
SqStack *s1,*s2;
LQueue *q;
ELEMTP x;
int flag;
s1=(SqStack *) malloc (sizeof(SqStack));
s2=(SqStack *) malloc (sizeof(SqStack));
q=(LQueue *) malloc (sizeof (LQueue));
InitStack_Sq(s1);
InitStack_Sq(s2);
InitQueue_L (q);
flag=True;
while(flag){
printf("请输入您的选择\n");
printf("S---------显示停车场状况\n");
printf("A------车辆到达\n");
printf("D------车辆离开\n");
printf("E------程序结束\n");
ch1=getchar();
switch (ch1){
case 'S': Display(s1,q);break;
case 'A': printf("输入数据:车牌号,到达时间:");
scanf("%d,%d",&x.num,&x.arrtime);
Arrive(s1,q,x);break;
case 'D': printf("输入数据:车牌号,离开时间:");
scanf("%d,%d",&x.num,&x.arrtime);
Delive(s1,s2,q,x);break;
case 'E': flag=False;
printf("程序正常结束\n"); break;
default: printf("输入数据错误,重新输入\n");
}
ch1=getchar();
}
}/*main*/
void InitStack_Sq (SqStack *s)
{s->top=0;
}
int Push_Sq(SqStack *s,ELEMTP x)
{ if (s->top==N)
return (False);
else
{s->elem[s->top]=x;s->top++;
return(True);
}
}
ELEMTP Pop_Sq(SqStack *s)
{ ELEMTP x;
if (s->top==0)
{ x.num=NULL;
x.arrtime=NULL;
return(x);
}
else
{ s->top--;
return (s->elem[s->top]);
}
}
void InitQueue_L(LQueue *q)
{ q->front=(QNode *)malloc(sizeof(QNode));
q->rear=q->front;
q->front->next=NULL;
q->front->num=0;
}
void EnQueue_L(LQueue *q,int num1)
{ QNode *p;
p=(QNode *)malloc(sizeof(QNode));
p->num=num1;
p->next=NULL;
q->rear->next=p;
q->rear=p;
q->front->num++;
}
int DelQueue_L(LQueue *q)
{ QNode *p;
int n;
if (q->front==q->rear)
return (NULL);
else
{ p=q->front->next;
q->front->next=p->next;
if (p->next==NULL)
q->rear=q->front;
n=p->num;
free(p);
q->front->num--;
return(n);
}
}
程序执行结果(加下划线的表示用户自己输入):
请输入您的选择
S------显示停车场状况
A------车辆到达
D------车辆离开
E------程序结束
A
输入数据:车牌号,到达时间:1234,5
请输入您的选择
S------显示停车场状况
A------车辆到达
D------车辆离开
E------程序结束
S
停车场状况:
车道 车号
1234
E
程序正常结束