本系列文章为浙江大学陈越、何钦铭数据结构学习笔记,前面的系列文章链接如下:
数据结构基础:P1-基本概念
数据结构基础:P2.1-线性结构—>线性表
数据结构基础:P2.2-线性结构—>堆栈
队列跟堆栈一样也是一种受限制的线性表。
它里面有最主要的有两个操作:一个叫做入队,一个叫做出队。数据插入我们称之为入队,数据删除我们称之为出队。插入跟删除分别发生在队列的两头,所以它表现出来的特点叫做先来先服务,所以它这种表又被称为先进先出表(FIFO)。
队列的抽象数据类型描述
类型名称:队列(Queue)
数据对象集:一个有0个或多个元素的有穷线性表。
操作集:长度为MaxSize的队列 Q ∈ Q u e u e {\rm{Q}} \in {\rm{Queue}} Q∈Queue,队列元素 i t e m ∈ E l e m e n t T y p e {\rm{item}} \in {\rm{ElementType}} item∈ElementType
Queue CreatQueue( int MaxSize ):生成长度为MaxSize的空队列;
int IsFullQ( Queue Q, int MaxSize ):判断队列Q是否已满;
void AddQ( Queue Q, ElementType item ): 将数据元素item插入队列Q中;
int IsEmptyQ( Queue Q ): 判断队列Q是否为空;
ElementType DeleteQ( Queue Q ):将队头数据元素从队列中删除并返回
队列的顺序存储实现
队列的顺序存储结构通常由一个一维数组和一个记录队列头元素位置的变量front
以及一个记录队列尾元素位置的变量rear
组成。对应的代码如下所示:
#define MaxSize <储存数据元素的最大个数>
struct QNode {
ElementType Data[ MaxSize ];
int rear; //实际上是数组的下标
int front;
};
typedef struct QNode *Queue;
举个例子
一些思考
我们可以看到,到最后队列入队那一段没空间了,不能入队。而出队列另一端还有空间。
我们需要想个办法来解决这个问题:一般而言我们的想法都是又返回去继续存储。
方案
我们把这个数组扳过来形成一个环,那么这就形成了顺环队列,其结构如下图所示:
当我们对顺环队列做操作时,如下图所示:
问题
这个数组分量是6个,我们现在已经放了5个元素了。如果这个时候我们再想放一个元素会发生什么?这个数组本身有一个空位,照道理是应该能加进去,但是加进去之后rear 等于1,front也等于1 ,front rear相等了。按照这样的组织方法 front rear相等是代表队列是空的。现在你加了一个元素,队列满了,front 和 rear也等于1。那么倒过来说, front rear如果相等,我问你队列是空的还是满的,你就搞不清楚了。
针对这个问题的思考:
(1)堆栈空和满的判别条件是什么?
我们是根据front rear的相对关系,也就是说它们的距离来判别的。front 和 rear的取值范围是0到n-1,对我们这个例子来讲是0到5。所以front跟rear的差距也是这6种情况 0到5,也就是说 、我这个数组如果大小是n的话,front跟rear之间的差距的情况就是n种。
(2)为什么会出现空、满无法区分?根本原因?
我们来看一下 队列有几种情况
对于我们这6个元素组成的数组来讲,队列的情况有7种:什么元素也没有、1个元素、2个元素、3个元素、4个元素、5个元素、6个元素。所以数组大小如果是等于n的话,队列的装载元素的情况有n+1种。而我们要判别队列空满或者它的队列放多少元素的状态是根据front rear的差距,这个差距只有n种情况。也就是你想用n种的状态来区分实际上存在的n+1种情况,怎么可能,一定有矛盾。就像我们用一个bit 来区分3种情况,这根本做不到。
解决方案:
(1)使用Size域或Tag域
①我们增加一个额外的标记Size来记录当前队列元素的个数。当你加入一个元素的时候 Size加1,删除一个元素的时候Size减1。所以我只要根据Size是等于0还是等于n,就可以知道是空的还是满的。
②我们增加一个额外的标记tag(0/1)。当你插入一个元素的时候Tag设为1,删除一个元素的时候tag等于0。所以当你front跟rear相等你搞不清楚是空或满的时候,你就要去看这个tag。这个tag就代表了最后一次操作是插入还是删除,那么你就知道了到底是空还是满。
(2)仅使用n-1个数组空间,这个时候也不会出现 front跟rear相等。
我们采用第二种方案,写出入队和出队的代码,如下:
入队列:
void AddQ( Queue PtrQ, ElementType item)
{
//(5+1)%6=0,这时候就满了
if ( (PtrQ->rear+1) % MaxSize == PtrQ->front ) {
printf("队列满");
return;
}
//只要没满,(PtrQ->rear+1)%Maxsize = (PtrQ->rear+1)
PtrQ->rear = (PtrQ->rear+1)% MaxSize;
PtrQ->Data[PtrQ->rear] = item;
}
出队列:
ElementType DeleteQ ( Queue PtrQ )
{
if ( PtrQ->front == PtrQ->rear ) {
printf("队列空");
return ERROR;
} else {
PtrQ->front = (PtrQ->front+1)% MaxSize;
return PtrQ->Data[PtrQ->front];
}
}
队列的另外一种实现方法就是链表,这跟我们堆栈一样的。所以同样地,我们可以用单向链表来实现我们的队列。这个时候同样的问题就冒出来了:队列有两个头,front 和 rear。链表有一个头和一个尾。那我们到底front设在链表的头还是front设在链表的尾。
这个问题跟我们前面堆栈的时候讨论过的问题是一样的。我们知道front是要做删除操作,rear是要做插入操作。
链表的头这个位置做插入或者删除都方便。做删除很快可以找到下一个结点,做插入则知道这个结点位置,可以直接插入。
链表的末尾做插入没有问题,做删除有问题。因为这是单向链表,你删了之后不知道前面一个结点在哪里。
因此,我们在链尾放rear,执行插入操作。在链首放front,执行删除操作。
队列的链式存储代码如下:
struct Node{
ElementType Data;
struct Node *Next;
};
struct QNode{ /* 链队列结构 */
struct Node *rear; /* 指向队尾结点 */
struct Node *front; /* 指向队头结点 */
};
typedef struct QNode *Queue;
Queue PtrQ;
不带头结点的链式队列出队操作的一个示例:
ElementType DeleteQ ( Queue PtrQ )
{
struct Node *FrontCell;
ElementType FrontElem;
if ( PtrQ->front == NULL) {
printf("队列空");
return ERROR;
}
FrontCell = PtrQ->front;
if ( PtrQ->front == PtrQ->rear) /* 若队列只有一个元素 */
PtrQ->front = PtrQ->rear = NULL; /* 删除后队列置为空 */
else
PtrQ->front = PtrQ->front->Next;
FrontElem = FrontCell->Data;
free( FrontCell ); /* 释放被删除结点空间 */
return FrontElem;
}
小测验
1、在一个链表表示的队列中, f和r分别指向队列的头和尾。下列哪个操作能正确地将s结点插入到队列中
A. f->next=s; f=s;
B. r->next=s; r=s;
C. s->next=r; r=s;
D. s->next=f; f=s;
答案:B
2、现采用大小为10的数组实现一个循环队列。设在某一时刻,队列为空且此时front和rear值均为5。经过若干操作后,front为8,rear为2,问:此时队列中有多少个元素?
A. 4
B. 5
C. 6
D. 7
答案:A
typedef int Position;
struct QNode {
ElementType *Data; /* 存储元素的数组 */
Position Front, Rear; /* 队列的头、尾指针 */
int MaxSize; /* 队列最大容量 */
};
typedef struct QNode *Queue;
Queue CreateQueue( int MaxSize )
{
Queue Q = (Queue)malloc(sizeof(struct QNode));
Q->Data = (ElementType *)malloc(MaxSize * sizeof(ElementType));
Q->Front = Q->Rear = 0;
Q->MaxSize = MaxSize;
return Q;
}
bool IsFull( Queue Q )
{
return ((Q->Rear+1)%Q->MaxSize == Q->Front);
}
bool AddQ( Queue Q, ElementType X )
{
if ( IsFull(Q) ) {
printf("队列满");
return false;
}
else {
Q->Rear = (Q->Rear+1)%Q->MaxSize;
Q->Data[Q->Rear] = X;
return true;
}
}
bool IsEmpty( Queue Q )
{
return (Q->Front == Q->Rear);
}
ElementType DeleteQ( Queue Q )
{
if ( IsEmpty(Q) ) {
printf("队列空");
return ERROR;
}
else {
Q->Front =(Q->Front+1)%Q->MaxSize;
return Q->Data[Q->Front];
}
}
typedef struct Node *PtrToNode;
struct Node { /* 队列中的结点 */
ElementType Data;
PtrToNode Next;
};
typedef PtrToNode Position;
struct QNode {
Position Front, Rear; /* 队列的头、尾指针 */
int MaxSize; /* 队列最大容量 */
};
typedef struct QNode *Queue;
bool IsEmpty( Queue Q )
{
return ( Q->Front == NULL);
}
ElementType DeleteQ( Queue Q )
{
Position FrontCell;
ElementType FrontElem;
if ( IsEmpty(Q) ) {
printf("队列空");
return ERROR;
}
else {
FrontCell = Q->Front;
if ( Q->Front == Q->Rear ) /* 若队列只有一个元素 */
Q->Front = Q->Rear = NULL; /* 删除后队列置为空 */
else
Q->Front = Q->Front->Next;
FrontElem = FrontCell->Data;
free( FrontCell ); /* 释放被删除结点空间 */
return FrontElem;
}
}