队列的定义精.docx
《队列的定义精.docx》由会员分享,可在线阅读,更多相关《队列的定义精.docx(18页珍藏版)》请在冰豆网上搜索。
队列的定义精
三、队列
1.队列的定义
队列(Queue)简称队,它也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。
我们把进行插入的一端称作队尾(rear),进行删除的一端称作队首(front)。
向队列中插入新元素称为进队或入队,新元素进队后就成为新的队尾元素;从队列中删除元素称为离队或出队,元素离队后,其后继元素就成为队首元素。
由于队列的插入和删除操作分别是在各自的一端进行的,每个元素必然按照进入的次序离队,所以又把队列称为先进先出表(FirstInFirstOut,简称FIFO)。
在日常生活中,人们为购物或等车时所排的队就是一个队列,新来购物或等车的人接到队尾(即进队),站在队首的人购到物品或上车后离开(即出队),当最后一人离队后,则队列为空。
例如,假定有a,b,c,d四个元素依次进队,则得到的队列为(a,b,c,d),其中字符a为队首元素,字符d为队尾元素。
若从此队中删除一个元素,则字符a出队,字符b成为新的队首元素,此队列变为(b,c,d);若接着向该队列插入一个字符e,则e成为新的队尾元素,此队列变为(b,c,d,e);若接着做三次删除操作,则队列变为(e),此时只有一个元素e,它既是队首元素又是队尾元素,当它被删除后队列变为空。
2.队列的存储结构
队列的存储结构同线性表和栈一样,既可以采用顺序结构,也可以采用链接结构。
(1)队列的顺序存储结构
队列的顺序存储结构需要使用一个数组和两个整型变量来实现,利用数组来顺序存储队列中的所有元素,利用两个整型变量来分别存储队首元素和队尾元素的下标位置,分别称它们为队首指针和队尾指针。
假定存储队列的数组用queue[QueueMaxSize]表示,队首和队尾指针分别用front和rear表示,则元素类型为ElemType的队列的顺序存储类型可定义为:
ElemTypequeue[QueueMaxSize];
intfront,rear;
其中QueueMaxSize为一个整型全局常量,需事先通过const语句定义,由它确定顺序队列(即顺序存储的队列)的最大长度,即最多能够存储的元素个数。
当然,队列的顺序存储空间也可以采用动态分配,此时用于决定最大长度的量可以为全局常量,也可以为全局或局部变量。
如在一个函数的函数体中使用下面语句能够为一个队列分配长度为n的数组空间,该数组名仍用queue表示。
ElemType*queue=newElemType[n];
队列的顺序存储类型同样可以用一个记录类型来表示,假定记录类型名为Queue,则该类型定义为:
structQueue{
ElemTypequeue[QueueMaxSize];
intfront,rear;
};
假定一个队列的当前状态如图4-10(a)所示,此时已经有a,b,c三个元素相继出栈(为了同队列中的元素相区别,把它们分别括了起来),队首指针front的值为3,指向的队首元素为d,队尾指针的值为7,指向的队尾元素为h;若接着插入一个新元素i,则队列的当前状态如图4-10(b)所示;若再接着删除一个元素,则变为图4-10(c)所示。
012345678QueueMaxSize-1
(a)(b)(c)defgh
frontrear
(a)
012345678QueueMaxSize-1
(a)(b)(c)defgh
i
frontrear
(b)
012345678QueueMaxSize-1
(a)(b)(c)(d)efgh
i
frontrear
(c)
图4-10顺序队列的插入和删除操作示意图
每次向队列插入一个元素,需要首先使队首指针后移一个位置,然后再向这个位置写入新元素。
当队尾指针指向数组空间的最后一个位置QueueMaxSize-1时,若队首元素的前面仍存在空闲的位置,则表明队列未占满整个数组空间,下一个存储位置应是下标为0的空闲位置,因此,首先要使队尾指针指向下标为0的位置,然后再向该位置写入新元素。
通过语句rear=(rear+1)%QueueMaxSize可使存储队列的整个数组空间变为首尾相接的一个环(称此为循环队列),当rear指向最后一个存储位置时,下一个所求的位置自动为数组空间的开始位置(即下标为0的位置)。
同对线性表和栈的插入一样,每次在进行队列插入前,也要判断队列是否已满(即数组空间是否已被用完),若是则停止插入,终止程序运行,否则可向队列中插入新元素。
若队尾指针的下一个位置(采用(rear+1)%QueueMaxSize计算出来)恰是队首指针front所指的位置,则表明队列已满,可知判断队满的条件是(rear+1)%QueueMaxSize==front。
从另一方面看,若队首指针和队尾指针已经指向了同一个位置,则表明队列中只有一个元素,当删除该元素后,队列为空,队尾指针不变,而队首指针指向了下一个位置,此时队尾指针的下一个位置正好也是队首指针所指的位置,由此可知,当条件(rear+1)%QueueMaxSize==front成立时,可能是队满的情况,也可能是队空的情况。
为了区别这两种情况,可设置一个标记,当进行插入操作后,置该标记为1,当进行删除操作后,置该标记为0。
在标记为1的情况下,上述条件成立则表明队列已满,在标记为0的情况下,上述条件成立则表明队列为空。
为了省去设置一个标记的麻烦,通常采用的处理方法是:
让front指针不是指向队首元素的位置,而是指向它的前一个位置,当上述条件成立时队列必然为满,当队首指针等于队尾指针时队列为空,此时整个数组空间只能利用QueueMaxSize-1个存储位置,而不是QueueMaxSize个存储位置。
在顺序队列中进行插入和删除时,不需要比较和移动任何元素,只需要修改队尾和队首指针,并向队尾写入元素或从队首取出元素,所以其时间复杂性为O
(1)。
(2)队列的链接存储结构
队列的链接存储结构也是通过由结点构成的单链表实现的,此时只允许在单链表的表头进行删除和在单链表的表尾进行插入,因此它需要使用两个指针:
队首指针front和队尾指针rear。
用front指向队首(即表头)结点的存储位置,用rear指向队尾(即表尾)结点的存储位置。
用于存储队列的单链表简称链接队列或链队。
假定链队中的结点类型仍采用第二章定义的LNode结点类型,那么队首和队尾指针为LNode*指针类型。
若把一个链队的队首指针和队尾指针定义在一个记录类型中,并假定该记录类型用标识符LinkQueue表示,则定义如下:
structLinkQueue{
LNode*front;
LNode*rear;
};
其中LNode结点类型重写如下:
structLNode{
ElemTypedata;
LNode*next;
};
设一个队列为(a,b,c),则对应的链接存储结构如图4-12(a)所示,当向该链队插入一个元素d后,对应如图4-12(b)所示,当接着从中删除队首元素a后,对应如图4-12(c)所示。
图4-12链队的插入和删除操作示意图
对链队的插入和删除操作同样不需要比较和移动元素,只需要修改个别相关指针和进行结点的动态分配或回收操作,所以其时间复杂度为O
(1)。
另外,使用链队不存在队满的问题,因为它使用的结点是动态分配的,只要内存中动态存储区仍有可用空间,就可以得到一个新结点,使之插入到链队中;链队也可能为空,此时front和rear指针均为空。
3.队列的抽象数据类型
队列的抽象数据类型中的数据部分为具有ElemType元素类型的一个队列,它可以采用任一种存储结构实现;操作部分包括元素进队、出队、读取队首元素、检查队列是否为空等。
下面给出队列的抽象数据类型的具体定义:
ADTQUEUEis
Data:
采用顺序或链接方式存储的栈,假定其存储类型
用QueueType标识符表示。
Operation:
voidInitQueue(QueueType&Q);
//初始化队列Q,即置Q为空
voidClearQueue(QueueType&Q);
//清除队列Q中的所有元素,使之成为一个空队
intQueueEmpty(QueueType&Q);
//判断Q是否为空,若是则返回1,否则返回0
ElemTypeQFront(QueueType&Q);
//返回队首元素,但不改变队列状态
voidQInsert(QueueType&Q,constElemType&item);
//将新元素item的值插入到队尾
ElemTypeQDelete(QueueType&Q);
//从队列Q中删除队首元素并返回之
intQueueFull(Queue&Q)
//若队列已满则返回1,否则返回0,此函数为顺序
//存储的队列所特有
endQUEUE
假定队列q的元素类型为整型int,下面给出调用上述操作的一些例子。
(1)InitQueue(q);//把队列置空
(2)QInsert(q,20);//元素20进队
(3)intx=5;QInsert(q,10*x);//元素10*x的值50进队
(4)cout<(5)QDelete(q);//删除队首元素20
(6)cout<<(x=QDelete(q))<//删除队首元素50,把它赋给x同时输出
(7)x=QueueEmpty(q);//因队列为空,返回1并赋给x
(8)while(!
QueueEmpty(q))cout<//列q中的所有元素,因q已经为空,所以不会得到任何输出
4.队列运算的实现
同线性表和栈一样,队列运算(操作)的具体实现取决于队列的存储结构,存储结构不同,其算法描述也不同。
下面分别给出队列的运算在顺序和链接两种存储结构上的实现的算法。
(a)队列运算在顺序存储结构上的实现
假定采用Queue记录类型的对象Q来表示顺序存储的队列,则在Q上进行各种队列运算的算法描述如下:
(1)初始化队列
voidInitQueue(Queue&Q)
//把队首和队尾指针置为同一下标值0,表示队空
{
Q.front=Q.rear=0;
}
(2)把一个队列清除为空
voidClearQueue(Queue&Q)
//对于顺序队列,此算法与初始化队列的算法相同
{
Q.front=Q.rear=0;
}
(3)检查一个队列是否为空
intQueueEmpty(Queue&Q)
//当队首与队尾指针相同时表示队空,返回1,否则返回0
{
returnQ.front==Q.rear;
}
(4)读取队首元素
ElemTypeQFront(Queue&Q)
//返回队首元素,但不改变队列状态
{
if(Q.front==Q.rear)
{//队列为空时退出程序运行
cerr<<"Queueisempty!
"<exit
(1);
}
returnQ.queue[(Q.front+1)%QueueMaxSize];
//队首元素是队首指针的下一个位置中的元素
}
(5)向队列插入元素
voidQInsert(Queue&Q,constElemType&item)
//将新元素item的值插入到队尾
{
intk=(Q.rear+1)%QueueMaxSize;
//求出队尾的下一个位置
if(k==Q.front)
{//若对列已满则终止程序运行
cerr<<"Queueoverflow!
"<exit
(1);
}
Q.rear=k;//修改队尾指针使之指向下一个位置
Q.queue[k]=item;//item的值被赋给新的队尾位置
}
(6)从队列中删除元素
ElemTypeQDelete(Queue&Q)
//删除队首元素并返回之
{
if(Q.front==Q.rear)
{//若队列为空则终止运行
cerr<<"Queueisempty!
"<exit
(1);
}
Q.front=(Q.front+1)%QueueMaxSize;
//修改队首指针使之后移一个位置
returnQ.queue[Q.front];
//返回队首元素
}
(7)检查一个队列是否已满
intQueueFull(Queue&Q)
//若队列已满则返回1,否则返回0,此函数为顺序队列所特有
{
return(Q.rear+1)%QueueMaxSize==Q.front;
}
(b)队列运算在链接存储结构上的实现
假定采用LinkQueue类型的对象HQ来表示链接存储的队列,则在HQ上进行各种队列运算的算法描述如下:
(1)初始化链队
voidInitQueue(LinkQueue&HQ)
//初始化链队,即把队首和队尾指针置为空,则为置队空
{
HQ.front=HQ.rear=NULL;
}
(2)清除链队为空
voidClearQueue(LinkQueue&HQ)
//清除链队中的所有结点,使之成为空队
{
LNode*p=HQ.front;
while(p!
=NULL){
HQ.front=HQ.front->next;
deletep;
p=HQ.front;
}//此循环结束后,所有结点被清除,队首指针变为空
HQ.rear=NULL;//置队尾指针为空
}
(3)检查链队是否为空
intQueueEmpty(LinkQueue&HQ)
//若链队为空则返回1,否则返回0
{
returnHQ.front==NULL;
//判断队首或队尾任一个指针是否为空即可
}
(4)读取队首元素
ElemTypeQFront(LinkQueue&HQ)
//读取链队中的队首元素
{
if(HQ.front==NULL)
{//若链队为空则终止执行
cerr<<"Linkedqueueisempty!
"<exit
(1);
}
returnHQ.front->data;//返回队首元素
}
(5)向链队中插入一个元素
voidQInsert(LinkQueue&HQ,constElemType&item)
//使新元素item的值插入链队
{
LNode*newptr=newLNode;
//得到一个由newptr指针所指向的新结点
if(newptr==NULL){//若内存动态存储空间用完则终止程序
cerr<<"Memoryallocationfailare!
"<exit
(1);
}
newptr->data=item;//把item的值赋给新结点的值域
newptr->next=NULL;//把新结点的指针域置空
if(HQ.rear==NULL)
HQ.front=HQ.rear=newptr;
//若链队为空,则新结点既是队首结点又是队尾结点
else
HQ.rear=HQ.rear->next=newptr;//依次修改队尾结点的
//指针域和队尾指针使之指向新的队尾结点
}
(6)从队列中删除一个元素
ElemTypeQDelete(LinkQueue&HQ)
//从链队中删除队首元素
{
if(HQ.front==NULL){//若链队为空则终止运行
cerr<<"Linkedqueueisempty!
"<exit
(1);
}
ElemTypetemp=HQ.front->data;//暂存队首元素以便返回
LNode*p=HQ.front;//暂存队首指针以便回收队首结点
HQ.front=p->next;//使队首指针指向下一个结点
if(HQ.front==NULL)
HQ.rear=NULL;//若链队变为空,则需同时使队尾指针变为空
deletep;//回收原队首结点
returntemp;//返回被删除的队首元素
}
5.使用队列的程序举例
程序1:
#include
#include
constintQueueMaxSize=6;
typedefintElemType;
structQueue{
ElemTypequeue[QueueMaxSize];
intfront,rear;
};
#include"queue.h"//该头文件保存着对顺序队列进行各种操作的算法
voidmain()
{
Queueq;
InitQueue(q);
for(inti=0;i<6;i++)
{
intx=rand()%100;
inty=rand()%100;
if(!
QueueFull(q)){
QInsert(q,x);
cout<}
if(!
QueueFull(q)){
QInsert(q,y);
cout<}
cout<cout<}
cout<while(!
QueueEmpty(q)){
cout<}
}
此程序使用一个长度为6的顺序队列,利用此队列保存由计算机产生的随机数。
主函数中的for循环体共执行6次,每次执行时首先产生出两个100以内的随机整数,接着在队列未满时入队,然后进行一次出栈操作,在主函数的最后使队列中的所有元素依次出队。
该程序的运行结果为:
41进队,67进队
41出队
34进队,0进队
67出队
69进队,24进队
34出队
78进队,58进队
0出队
62进队,
69出队
5进队,
24出队
78出队
58出队
62出队
5出队
程序2:
#include
#include
typedefintElemType;
structLNode{
ElemTypedata;
LNode*next;
};
structLinkQueue{
LNode*front;
LNode*rear;
};
#include"linkqueue.h"//此头文件保存着对链队的各种操作的算法
voidmain()
{
LinkQueueq1,q2;
InitQueue(q1);
InitQueue(q2);
for(inti=0;i<20;i++)
{
intx=rand()%100;
cout<if(x%2==1)
QInsert(q1,x);//用q1链队存放奇数
else
QInsert(q2,x);//用q2链队存放偶数
}
cout<while(!
QueueEmpty(q1)&&!
QueueEmpty(q2))
{//每一行输出一个奇数和一个偶数,直到任一队列空为止
cout<}
}
此程序使用了两个链队q1和q2,用来分别存储由计算机随机产生的20个100以内的奇数和偶数,然后每行输出q1和q2中的一个值,即奇数和偶数配对输出,直到任一队列为空时止。
该程序的运行结果为:
41673406924785862645458127619195422736
4134
670
6924
578
4558
8162
2764
6142
9136
6.队列的应用简介
在后面的章节中将会看到队列在具体算法中的应用,这里仅从两个方面来简述队列在计算机科学领域所起的作用。
第一个方面是解决主机与外部设备之间速度不匹配的问题,第二个方面是解决由多用户引起的资源竞争问题。
对于第一个方面,仅以主机和打印机之间速度不匹配的问题为例作一下简要说明。
主机输出数据给打印机打印,输出数据的速度比打印数据的速度要快得多,若直接把输出的数据送给打印机打印,由于速度不匹配,显然是不行的。
所以解决的方法是设置一个打印数据缓冲区,主机把要打印输出的数据依次写入到这个缓冲区中,写满后就暂停输出,转去做其它的事情;打印机就从缓冲区中按照先进先出的原则依次取出数据并打印,打印完后再向主机发出请求,主机接到请求后再向缓冲区写入打印数据,这样做既保证了打印数据的正确,又使主机提高了效率。
由此可见,打印数据缓冲区中所存储的数据就是一个队列。
对于第二个方面,CPU(即中央处理器,它包括运算器和控制器)资源的竞争就是一个典型的例子。
在一个带有多终端的计算机系统上,有多个用户需要CPU各自运行自己的程序,它们分别通过各自终端向操作系统提出占用CPU的请求,操作系统通常按照每个请求在时间上的先后顺序,把它们排成一个队列,每次把CPU分配给队首请求的用户使用,当相应的程序运行结束或用完规定的时间间隔后,则令其出队,再把CPU分配给新的队首请求的用户使用,这样既满足了每个用户的请求,又使CPU能够正常运行。
关于队列在这两个方面应用的详细情况将会在操作系统课程中讨论。