队列结构解析及其应用
导言
队列在生活中处处可见,例如在食堂买饭,你需要排队,先来的同学先买,后面的同学需要等前面的同学买好才能够前进(这不废话吗)。对于程序设计,队列的思想应用广泛,例如在操作系统中的作业排队也是使用队列来实现的,在一个允许多道程序运行的计算机系统中,面对多个运行的作业,它们就需要按照请求输入的次序排队,当通道阐述完毕时,队头的作业就先出队列进行输出操作。
队列
有别于栈,队列 (queue) 是只允许在一端进行插入操作,在另一端进行删除操作的线性表,核心思想是先进先出,其中允许插入操作的一端被称为队尾 (rear),允许删除操作的一端被称为队头 (front)。
队列的抽象数据类型
ADT Queue
{
Data:
D = {ai | 1 ≤ i ≤ n, n ≥ 0, ai 为 ElemType 类型} //同线性表
Relation:
R = { <ai,ai+1> | ai,ai+1 ∈ D, i = 1, i ∈ (0,n)} //同线性表
Operation:
InitQueue(&q); //初始化队列,开辟一个空间给队列 q
QueueEmpty(*q); //判断栈是否为空队列,若为空队列返回 true,否则返回 false
EnQueue(&q,e); //入队列操作,将元素 e 加入队列结构中并使其成为队尾元素
DeQueue(&q,&e); //出队列操作,将位于队列头的元素删除,并赋值给变量 e
GetHead(q,&e); //取队头操作,若栈不为空队列,返回队列头元素并赋值给变量 e
ClearQueue(&q); //清空队列,将栈中的所有元素清空,即将队列变为空队列
DestroyQueue(&q); //销毁队列,将释放队列的空间
QueueLength(q); //返回队列元素个数
}
顺序队列及其基本操作
假溢出
与顺序栈相似,由于队列本质上也是个线性表,因此我们对于顺序存储往往使用数组来描述,因此我们需要为一个数组设置队列头和队列尾,需要分别定义队头、队尾指针作为游标来辅助。初始化时,我们令 front = rear = 0,每当有元素入队列时,尾指针 rear 增加1,有元素出队列时,头指针 front 增加1,这样就能保证头指针始终指向队列头元素,尾指针始终指向队列尾元素,这样队列的头尾就说清楚了。
顺序队列的结构体定义
#define MAXSIZE 100
typedef struct
{
ElemType data[MAXSIZE];
int front; //队列头指针
int rear; //队列尾指针
}SqQueue;
但是这样会出现一个很严重的问题,假设有如图所示队列(MAXSIZE = 5),我们入队 5 个元素,然后出队 4 个元素,那么队列的状态就会变为图示的状态,此时如果继续有元素入队的话,就会因数组越界而发生溢出的情况,但是我们发现队列还是有很多的空闲空间的,这就说明我们的对列空间没有得到充分的利用,这是由“队尾入队,对头出队”的操作限制引起的。
顺序队列
对于假溢出问题,解决的思路很明确,就是我们需要实现某种机制让我们能回到数组下标为 0 的位置继续使用空闲的空间即可,也就是说我们需要一些代码让我们的队头指针和队尾指针复位,由于数组的长度我们可知,因此我们可以通过取模的方式来实现这种操作。当我们使用这种方法解决假溢出的问题时,这种队列结构也被称为循环队列,但是其本质只是添加了复位功能的队列而已。
如何判断空队列
因此可见,对于一个循环队列我们不能单纯地使用头指针或尾指针的值来描述空队列,对于这个问题有两种解决方案:
一、少使用一个数组空间,也就是说当数组中的元素数量达到 MAXSIZE - 1 时就认为是队列满,采用这种机制时,头尾指针数值相同时认为是空队列:
Q.front = Q.rear;
而尾指针数值加1之后等于头指针的数值时,认为队满:
(Q.rear + 1) % MAXSIZE == Q.front;
二、设置一个标志位来盘对是否为空队列。
初始化队列
构造一个空队列,分配一个最大容量是 MAXSIZE 的数组空间,头指针和尾指针的初始化为0,表示这是空队列。
void InitQueue(Queue &q)
{
q = new SqQueue;
q->front = q->reat = 0;
}
求队列长度
由于我们使用循环队列的描述方式,因此尾指针的值可能比头指针的数值小,也就是说尾指针与头指针的数值之差可能是负数,因此就需要对这个差值加上 MAXSIZE 之后对 MAXSIZE 求余。
int QueueLength(SqQueue q)
{
return (q.rear - q.front + MAXSIZE) % MAXSIZE;
}
入队列
在队尾插入一个新元素,若队满则无法插入,返回 false,否则返回 true。
bool EnQueue(SqQueue &q,ElemType e)
{
if((q,rear + 1) % MAXSIZE == q.front) //判断是否队列满
{
return false;
}
q.data[q.rear] = e;
q.rear = (q.rear + 1) % MAXSIZE;
return true;
}
出队列
将队列头的元素删除并赋值给 e,若为空队列则返回 false,否则返回 true.
bool DnQueue(SqQueue &q,ElemType e)
{
if(q.front == q.rear) //判断是否为空队列
}
取队列头元素
ElemType GetHead(SqQueue q)
{
if(q.front != q.rear) //判断是否是空队列
return q.data[q.front];
}
链队列
对于用链表描述的队列,我们需要两个指针分别指向队列头和队列尾,为了便于描述我们将添加一个头结点,用头指针指向。
链队列的结构体定义
typedef struct QueueNode
{
ElemType data;
struct QueueNode *next;
}Node,*QueuePtr;
typedef struct
{
QueuePtr front; //头指针
QueuePtr rear; //尾指针
}LinkQueue;
初始化队列
构造一个只有头结点的空队列,头指针和尾指针均指向头结点,头结点的指针域为 NULL。
void InitQueue(LinkQueue &q)
{
q.front = q.rear = new Node; //头指针和尾指针均指向头结点
q.front->next = NULL; //头指针的后继为 NULL
}
入队列
申请一个新结点,新结点的数据域为 e,通过尾插法的方式插入链队列中。对于链队列而言,不需要判断是否队满。
bool EnQueue(LinkQueue &q,ElemType e)
{
QueuePtr ptr = new Node;
if(ptr = NULL)
{
return false;
}
ptr->data = e;
ptr->next = NULL;
q.rear->next = ptr; //尾插法插入结点
q.rear = ptr; //修改尾指针
return true;
}
出队列
将链队列的表头结点的空间释放,若为空队列返回 false,否则返回 true。
bool DnQueue(LinkQueue &q,ElemType e)
{
QueuePtr ptr;
if(q.front == q,rear) //判断是否是空队列
return false;
ptr = q.front->next;
e = ptr->data;
q.front->next = ptr->next; //修改头结点的后继
if(q,rear == p) //若出队列操作后,队列为空队列,令尾指针指向头结点
q.rear = q.front;
delete ptr;
return true;
}
准确描述头尾指针
实现目标
应用解析
在不设置尾指针的情况下,我们该这么描述尾指针呢?如果你对尾指针和头指针存在的意义理解透彻的话,你就能明白,队列中的元素个数我们用“(q.rear - q.front + MAXSIZE) % MAXSIZE”来描述,现在我们只是需要反过来实现而已。
代码实现
入队列
bool AddQ(Queue Q, ElementType X)
{
if (Q->MaxSize == Q->Count)
{
printf("Queue Full\n");
return false;
}
Q->Count++;
Q->Data[(Q->Front + Q->Count) % Q->MaxSize] = X;
return true;
}
出队列
ElementType DeleteQ(Queue Q)
{
if (Q->Count == 0)
{
printf("Queue Empty\n");
return ERROR;
}
Q->Count--;
Q->Front = (Q->Front + 1) % Q->MaxSize;
return Q->Data[Q->Front];
}
队列应用-舞伴问题
应用情景
代码实现
int QueueLen(SqQueue Q) //取队列长度
{
return (Q->rear - Q->front);
}
int EnQueue(SqQueue& Q, Person e) //入队列
{
Q->data[Q->rear++] = e;
return 1;
}
int QueueEmpty(SqQueue& Q) //判断空队列
{
return !(Q->rear - Q->front);
}
int DeQueue(SqQueue& Q, Person& e) //出队列
{
e = Q->data[Q->front++];
return 1;
}
void DancePartner(Person dancer[], int num) //舞伴配对
{
Person people[2];
for (int i = 0; i < num; i++)
{
if (dancer[i].sex == 'F')
{
EnQueue(Fdancers, dancer[i]);
}
else
{
EnQueue(Mdancers, dancer[i]);
}
}
while (!QueueEmpty(Mdancers) && !QueueEmpty(Fdancers))
{
DeQueue(Fdancers, people[0]);
DeQueue(Mdancers, people[1]);
cout << people[0].name << " " << people[1].name << endl;
}
}
队列应用-银行排队模拟
左转我的另一篇博客——PTA习题解析——银行排队问题
队列应用-迷宫寻路(广度优先)
左转博客——栈和队列应用:迷宫问题
队列应用-优先级队列
阅读代码部分,左转我另一篇博客数据结构——堆
参考资料
《大话数据结构》—— 程杰 著,清华大学出版社
《数据结构教程》—— 李春葆 主编,清华大学出版社
《数据结构与算法》—— 王曙燕 主编,人民邮电出版社
《数据结构(C语言版|第二版)》—— 严蔚敏 李冬梅 吴伟民 编著,人民邮电出版社