队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头
在我们利用键盘进行字母数字的输入显示到显示屏上其实就是利用了队列的形式,以及当我们电脑卡死的时候,我们鼠标乱点的操作其实都是记录了起来,等着电脑正常的时候,这些操作又会再去继续执行
队列的抽象数据类型定义如下所示
ADT 队列(Queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitQueue(*Q): 初始化操作,建立一个空队列Q。
DestroyQueue(*Q): 若队列Q存在,则销毁它。
ClearQueue(*Q): 将队列Q清空。
QueueEmpty(Q): 若队列Q为空,返回true,否则返回false。
GetHead(Q, *e): 若队列Q存在且非空,用e返回队列Q的队头元素。
EnQueue(*Q, e): 若队列Q存在,插入新元素e到队列Q中并成为队尾元素。
DeQueue(*Q, *e): 删除队列Q中队头元素,并用e返回其值
QueueLength(Q): 返回队列Q的元素个数
endADT
线性表有顺序存储和链式存储,栈是线性表所以也有顺序存储和链式存储这两种存储方式,队列也是一种特殊的线性表,也同时存在这两种存储方式
对于顺序存储的队列的入队操作其实就是在队尾去增加一个元素,不需要去移动任何元素,因此时间复杂度为O(1),队列元素的出列操作是在队头,也就是下标为0的位置,这就就是说如果出队第一个元素出队了,其他元素都要往前移,所以这个时候空间复杂度为O(n)
当然如果我们去规定队头不一定在下标为0处,那么后面也就不用移动了,但是这样的话前面因为出队的空间就会浪费了,这样的话到了队尾就真的是到了队尾
基于此问题,我们去使用循环队列的方式去解决,对于循环队列的判断如果是队列满的判断就是(rear+1)%QueueSize == front,如果是计算队列的长度就是(rear-front+QueueSize)%QueueSize
关于循环队列的定义如下所示,下面的Status是int类型,OK是1是一个宏定义,ERROR也是宏定义表示0
/* QElemType类型根据实际情况而定,这里假设为int */
typedef int QElemType;
/* 循环队列的顺序存储结构 */
typedef struct
{
QElemType data[MAXSIZE];
/* 头指针 */
int front;
/* 尾指针,若队列不空,
指向队列尾元素的下一个位置 */
int rear;
} SqQueue;
初始化一个空队列
/* 初始化一个空队列Q */
Status InitQueue(SqQueue *Q)
{
Q->front = 0;
Q->rear = 0;
return OK;
}
循环队列求队列长度
/* 返回Q的元素个数,也就是队列的当前长度 */
int QueueLength(SqQueue Q)
{
return (Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}
循环队列的入队操作如下所示
Status EnQueue(SqQueue *Q,QElemType e)
{
//队列满的判断
if((Q->rear+1)%MAXSIZE==Q->front)
{
return ERROR;
}
//将e的值赋值给队尾,因为这里采取的是保留一个元素空间的方法来判断队列是否满了
Q->data[Q->rear]=e;
//rear指针向后移动一个位置
Q->rear = (Q->rear+1)%MAXSIZE;
}
队列出队的操作
/* 若队列不空,则删除Q中队头元素,用e返回其值 */
Status DeQueue(SqQueue *Q, QElemType *e)
{
//判断队列是否为空
if(Q->front==Q->rear)
return ERROR;
//将队头的元素赋值给e
*e=Q->data[Q->front];
//front指针向后移动一个位置
Q->front=(Q->front+1)%MAXSIZE;
return OK;
}
如果我们只是去对队列使用顺序存储,不使用循环队列,算法的时间性能是不高的,但是循环队列的话我们还是采取顺序存储我们就需要去考虑空间的问题怎么去分配,所以我们可以去采取队列的链式存储结构
队列的链式存储结构,其实就是线性表的单链表,只不过它只能位进头出而已,我们把它简称为链队列,我们将队头指针指向链队列的头结点,将队尾指针指向终端结点
如果是空队列的时候,front和rear都是指向头结点的
关于链队列的结构定义如下所示
typedef int QElemType;
/* 结点结构 */
typedef struct QNode
{
QElemType data;
struct QNode *next;
} QNode, *QueuePtr;
/* 队列的链表结构 */
typedef struct
{
/* 队头、队尾指针 */
QueuePtr front, rear;
} LinkQueue;
在队列的链式存储结构当中的入队操作其实就是在链表的尾部插入结点,就是将rear的指向往后移动一个位置
Status DeQueue(LinkQueue *Q, QElemType *e)
{
QueuePtr p;
if (Q->front == Q->rear)
return ERROR;
/* 将欲删除的队头结点暂存给p */
p = Q->front->next;
/* 将欲删除的队头结点的值赋值给e */
*e = p->data;
/* 将原队头结点后继p->next赋值给头结点后继, */
Q->front->next = p->next;
/* 见上图中② */
/* 若队头是队尾,则删除后将rear指向头结点*/
if (Q->rear == p)
Q->rear = Q->front;
free(p);
return OK;
}
对于链式存储的队列的出队操作如下所示
Status DeQueue(LinkQueue *Q, QElemType *e)
{
QueuePtr p;
if (Q->front == Q->rear)
return ERROR;
/* 将欲删除的队头结点暂存给p*/
p = Q->front->next;
/* 将欲删除的队头结点的值赋值给e */
*e = p->data;
/* 将原队头结点后继p->next赋值给头结点后继, */
Q->front->next = p->next;
/* 若队头是队尾,则删除后将rear指向头结点*/
if (Q->rear == p)
Q->rear = Q->front;
free(p);
return OK;
}
一般来说如果在可以确定队列长度最大值的情况下,建议用循环队列,如果我们无法估计队列的长度的时候,一般采取链队列