目录
一、前情提要
上一篇讲到了一种特殊的线性表,叫做栈。像栈一样,队列(queue)也是一种特殊的线性表。不同的是:入栈和出栈的顺序是先进后出,而入队和出队的顺序是先进先出。本篇会介绍这种数据结构并给出实现代码。
二、队列ADT的介绍与分析
(一)什么是队列?
定义:队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
从这个定义我们就能看出来和栈的区别,栈的定义是:限定仅在表尾进行插入和删除操作的线性表。
所以我们就可以总结出队列的性质:
1.队列是一种先进先出(First In First Out)的线性表,简称FIFO。
2.只允许插入的一端称为队尾,允许删除的一端称为队头。
我们画张图理解一下:
(二)队列的抽象数据类型
同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,删除数据只能在队头进行。
ADT 队列(queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitQueue(*Q):初始化操作,建立一个空队列。
DestroyQueue(*Q):若队列存在,则销毁它。
ClearQueue(*Q):将队列清空。
QueueEmpty(Q):若队列为空,返回true,否则返回false。
GetHead(Q):若队列存在且非空,返回Q的队头元素。
Push(*Q,*e):若队列存在,插入新元素e到队列中并成为队尾元素。
Pop(*Q,*e):删除队列中队头元素,并返回其值。
QueueLength(S):返回队列的元素个数。
endADT
(三)队列的存储结构
线性表有顺序存储和链式存储,栈是线性表,具有这两种存储方式。同样的,队列作为一种特殊的线性表,也同样存在这两种存储方式。我们先来看队列的顺序存储结构。
先来说说顺序存储:
队列的顺序存储结构
队列顺序存储的不足:
我们假设一个队列有n个元素,则顺序存储的队列需要建立一个大于n的数组,并把所有的元素存储在前n个单元内,数组下标为0的一端就是队头。
首先入队操作,就是在队尾追加一个元素,此时不需要移动任何元素,因此只需要O(1)的时间复杂度就可以完成。
其次出队操作,就是在队头弹出一个元素,即在下标为0的位置操作。
我画张图看一下:
我们会发现第一个位置元素出队之后,就空着了,队头还在下标为0的位置处。没办法,我们只能将后面的元素集体向前挪一位,给他填满。
这样出队的时候就要挪动所有元素,所以出队操作的时间复杂度是O(n)。
这样的话出队操作太慢了!!!
我们可以考虑用Front和Rear指针来进行优化。
Front指针指向队头元素,Rear指针指向队尾元素
所以就优化之后的图会变成这样:
直接移动Front指针即可。
Q:为啥Rear指针要在队尾元素的后一位?
A:为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以Rear指针指向队尾元素的下一位。这样一来,当Front和Rear指向一个地方时,此队列并不是还剩一个元素,而是空队列!!
你以为优化完了吗????
我们知道Rear指针是指向队尾元素的后一个位置,如果队尾元素已经是最后一个单元格了呢?Rear难道移动到数组之外吗?显然还存在问题。
如图:
第一个问题:Rear指针会移动到数组之外。
还存在一个问题:如果元素继续增加,新元素a7该放在哪?目前如果接着入队的话,因为数组末尾元素已经占用,再向后加,就会产生数组越界的错误,可实际上我们队列在下标为0的地方还是空闲的。我们把这种现象叫做假溢出。
在现实生活中,你上了一辆公交车,发现前排有一个空座位,而后排全部坐满了,你会怎么做?下车不做了,等下一辆?没有这么笨的人,前面有座位,当然是可以坐的,除非坐满了,才会考虑下一辆。
为了解决这两个问题,迎来了队列顺序存储结构的最终改良版本———循环队列。
循环队列的定义:解决假溢出的办法就是后面满了,咱就从前面开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。
刚才的例子继续,上图中的Rear指针移动到队尾元素的下一个位置,因为是头尾相接的,最终它可以指向下标为0的位置。
我们最后再添加一个元素,让它变满。
此时问题又来了:我们刚刚说,空队列时,Front等于Rear,现在当队列满的时候,也是Front等于Rear,那么如何判断此时的队列究竟是空队列还是满的呢?
方法一:直接设置一个flag标志变量,当Front==Rear,且flag=0时为队列空,当Front==Rear,且flag=1时队列满。
方法二(一般用这个):当队列空时,条件就是Front==Rear,当队列满的时候,我们修改其条件,保留一个元素空间。也就是说,队列满时,数组中还存在一个空闲单元。例如下图中的情况(至少还存在一个空位,无论这个空位在哪),我们就认为队列已经满了,也就是说我们不允许上图情况(所有单元格都被占)的情况发生。
我们重点来讨论第二种方法,由于Rear可能比Front大,也可能比Front小,所以尽管它们只相差一个位置时就是满的情况,但也可能是相差一整圈。所以若队列的最大尺寸为QueueSize,那么队列满的条件就是(Rear+1)%QueueSize==Front(%取模的目的就是为了整合Rear和Front大小的一个问题)。
比如上面这个例子,QueueSize=7,Rear=0,Front=1,根据公式
(Rear+1)%QueueSize==Front (0+1)%7=1 符合条件,所以此时队列是满的。
还有一个公式,通用的计算队列长度公式:(Rear - Front + QueueSize)%QueueSize
最终代码会在最后给出。
接下来就是队列的链式存储结构:
队列的链式存储结构
队列的链式存储结构,其实就是线性表的单链表,只不过他只能尾进头出而已,我们把它简称为链队列。为了方便操作,我们将队头指针指向头结点,而队尾指针指向终端结点。
空队列的时候,Front和Rear都指向头结点。
这个入队和出队操作就很简单了,就是尾插和普通的删除操作。
直接看后面代码吧。
三、代码实现
(一)队列的顺序存储结构——循环队列
#include <stdio.h>
#include <stdlib.h>
#define MaxSize 10
typedef struct
{
int Data[MaxSize];
int Front;
int Rear;
}CircularQueue;
//队列初始化
void InitQueue(CircularQueue* Queue)
{
Queue->Rear=0; //首尾指针都指向同一位置
Queue->Front=0;
}
//入队操作
void Push(CircularQueue* Queue,int Data)
{
if((Queue->Rear+1)%MaxSize==Queue->Front)
{
printf("已满\n");//队列已满
}
else
{
Queue->Data[Queue->Rear]=Data; //输入当前结点数据
Queue->Rear=(Queue->Rear+1)%MaxSize;//尾指针向后移一位,取模是因为怕尾指针越界
}
}
//出队操作
void Pop(CircularQueue* Queue)
{
if(Queue->Front==Queue->Rear)
{
printf("空队列无法删除\n");
}
else
{
Queue->Front=(Queue->Front+1)%MaxSize;//头指针向后移一位。取模是因为怕指针越界
}
}
//输出队列长度
void QueueLength(CircularQueue* Queue)
{
if(Queue->Front==Queue->Rear)
{
printf("0\n");
}
else
{
printf("%d\n",(Queue->Rear-Queue->Front+MaxSize)%MaxSize);//由于尾指针可能再头指针前面,所以有可能为负值,取模转为正.
}
}
//查询队头的数据
void GetHead(CircularQueue* Queue)
{
if(Queue->Front==Queue->Rear)
{
printf("空队列\n");
}
else
{
printf("%d\n",Queue->Data[Queue->Front]);
}
}
//判断是否为空队列
void QueueEmpty(CircularQueue* Queue)
{
if(Queue->Front==Queue->Rear)
{
printf("为空队列\n");
return ;
}
else
{
printf("该队列不为空\n");
}
}
//清空队列
void ClearQueue(CircularQueue* Queue)
{
Queue->Rear=Queue->Front;//头指针和尾指针都放在一起,代表为空。
}
int main()
{
CircularQueue Queue;
InitQueue(&Queue);
Push(&Queue,1);
Push(&Queue,2);
Push(&Queue,3);
Push(&Queue,4);
GetHead(&Queue);
Pop(&Queue);
GetHead(&Queue);
QueueLength(&Queue);
QueueEmpty(&Queue);
ClearQueue(&Queue);
QueueEmpty(&Queue);
return 0;
}
(二)队列的链式存储结构——链队列
#include <stdio.h>
#include <stdlib.h>
typedef struct QueueNode
{
int Data;
QueueNode* Next;
}QNode,*QLinkList; //队列的结点,指向结点的指针
typedef struct LinkQueue
{
QLinkList Front,Rear;
}LinkQueue; //定义头指针和尾指针
//初始化队列
void InitQueue(LinkQueue* Queue)
{
Queue->Front=Queue->Rear=(QNode*)malloc(sizeof(QNode));//头指针和尾指针指向头结点
if(Queue->Front==NULL)
{
printf("内存分配失败");
}
else
{
Queue->Front->Next=NULL;//初始化头结点的Next,指向NULL
}
}
//入队操作
void Push(LinkQueue* Queue,int Data)
{
QNode* New_Node=(QNode*)malloc(sizeof(QNode));
if(New_Node==NULL)
{
printf("内存分配失败");
}
else
{
New_Node->Data=Data;//输入数据
New_Node->Next=NULL;//新结点的Next指向NULL
Queue->Rear->Next=New_Node;//尾指针指向的结点的Next指向新结点,建立联系
Queue->Rear=New_Node;//尾指针指向新结点
}
}
//出队操作
void Pop(LinkQueue* Queue)
{
if(Queue->Front==Queue->Rear)
{
printf("为空队列,无法出队");
}
else
{
QNode* Ptr=Queue->Front->Next; //先建立临时指针记录要出队的结点
Queue->Front->Next=Ptr->Next; //头指针指向要出队结点的下一个结点
if(Queue->Rear==Ptr) Queue->Rear=Queue->Front;//此处要特判一下,如果队中只有一个结点
//删除后要将头指针和尾指针一起指向头结点。
free(Ptr); //最后释放出队元素的内存
}
}
//输出队头元素
void GetHead(LinkQueue* Queue)
{
if(Queue->Front==Queue->Rear)
{
printf("为空队列\n");
}
else
{
printf("%d\n",Queue->Front->Next->Data);
}
}
//判断队列是否为空
void QueueEmpty(LinkQueue* Queue)
{
if(Queue->Front==Queue->Rear)
{
printf("为空队列\n");
}
else
{
printf("队列不为空\n");
}
}
//清空队列
void ClearQueue(LinkQueue* Queue)
{
QNode* p=Queue->Front->Next;
QNode* q;
while(p)
{
q=p->Next;
free(p);
p=q;
} //清空队列
Queue->Front->Next=NULL;//头结点的Next置为NULL
Queue->Rear=Queue->Front;//尾指针和头指针指向一块地方,表示队列为空。
}
//输出队列长度
void QueueLength(LinkQueue* Queue)
{
int Count=0;
QNode* Ptr=Queue->Front->Next;
while(Ptr)
{
Count++;
Ptr=Ptr->Next;
}
printf("%d\n",Count);
}
int main()
{
LinkQueue* Queue;
Queue=(LinkQueue*)malloc(sizeof(LinkQueue));
InitQueue(Queue);
Push(Queue,1);
Push(Queue,2);
Push(Queue,3);
Push(Queue,4);
Push(Queue,5);
GetHead(Queue);
QueueLength(Queue);
Pop(Queue);
GetHead(Queue);
QueueLength(Queue);
QueueEmpty(Queue);
ClearQueue(Queue);
QueueEmpty(Queue);
return 0;
}