目录
队列的基本概念
队列(Queue),简称“队”,是一种操作受限的线性表,其特点在于只允许在一端进行插入,而在另一端进行删除。将元素加入队列称为入队,而从队列中移除元素称为出队。
这一特性与我们日常生活中的排队方式类似——最先进入队列的元素最先被移除。队列遵循先进先出(First In, First Out,FIFO)的原则。
队列结构如下图所示:
其中几个重要概念如下:
队头(Front):允许删除的一端,又称队首。
队尾(Rear):允许插入的一端。
空队列:不含任何元素的空表。
顺序队列
队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针:
队头指针(front):指向队头元素。
队尾指针(rear):指向队尾元素的下一个位置。
但是更一般的情况下队头队尾指针不一定这样指向,在本文的最后一部分会详细说明。
顺序队列的结构体定义如下:
#define MaxSize 4 //队列最大存储容量
typedef struct{
int data[MaxSize-1]; //注意MaxSize-1,部分教材这里用的MaxSize,会混淆
int front, rear;
}SqQuene;
开始时Q.front=Q.rear=0,入队时队尾指针记录入队值后+1(因为队尾指针指向队尾元素的下一个位置。),出队时记录出队值后队头指针+1。
由于入队出队指针都是+1,可以预见的是如果5个元素入队并且出队,队头队尾指针都变成5,此时指针位置超出队列限值,出现“假溢出”情况,即队列里明明什么都没有但是指针还是超限。
于是这里给出循环队列的概念。
循环队列
定义
部分教材上将顺序队列臆造为一个环状的空间,即把存储队列元素的表从逻辑上视为一个环,称为循环队列。当指针到达队列末端,且还要再将一个元素入队时,将指针重置为数组的第一位,也就是跟个圆圈一样将数组头尾相连。
要实现这样的操作,可以使用计算机里专门的操作符——求余运算(%)。当队首指针需要进1时,令Q.front=(Q.front+1)%MaxSize;当队尾指针需要进1时,令Q.rear=(Q.rear+1)%MaxSize;
注意这里MaxSize为最大存储容量,有的教材前面代码定义部分与这里冲突了。
但有一个问题是,按照上面这种方式,队满队空时都有Q.front==Q.rear。于是下面给出一种常见的处理这种问题的方法:牺牲一个存储单元来区分队空队满。
当队满时,判定条件为(Q.rear+1)%Maxsize==Q.front。
当然,从做题的角度来说我更喜欢把循环队列视为一个具有“虫洞”的队列。当队的一头进入到队列存储结构的末端时,自动从队列的头部钻出。
其实很容易看出,Q.rear的值一定会比Q.front更大,如果Q.front大于Q.rear,说明队尾指针已经从“虫洞”钻出。
当然如果你把队列视作一个圆,那么队尾一定会在队头的顺时针方向(即存储的元素位于front顺时针到rear的环内)。
队列长度的通解公式为(Q.rear+MaxSize-Q.front)%MaxSize。
基本操作
初始化队列
void InitQuene(SqQuene &Q){
Q.front=Q.rear=0; //头尾指针置零
}
判队空
bool IsEmpty(SqQuene Q){
if(Q.rear==Q.front) //队头队尾指针相等时队列为空
return true;
else
return false;
}
入队
bool EnQuene(SqQuene &Q,int x){
if((Q.rear+1)%MaxSize==Q.front) //队满报错
return false;
Q.data[Q.rear]=x; //赋值
Q.rear=(Q.rear+1)%MaxSize; //重置尾指针
return true;
}
出队
bool DeQuene(SqQuene &Q,int &x){
if(Q.rear==Q.front)
return false; //队空判错
x=Q.data[Q.front];
Q.front=(Q.front+1)%MaxSize
return true;
}
链式队列
队列的链式表示称为链队列,它实际上是一个同时有队头指针和队尾指针的非循环单链表(当然带尾指针的循环单链表也可以表示队列【数据结构】链表的基本概念和操作)。
链式队列的定义如下:
typedef struct LinkNode{ //定义队列节点
int data;
struct LinkNode *next //指针域
}LinkNode;
typedef struct{ //定义队列
LinkNode *front,*rear; //队头队尾
}LinkQuene;
这里需要注意链式队列的定义使用了两个结构体,一个结构体表示队列节点单元,另一个表示队列头尾指针。那么为什么链栈只需要使用一个结构体呢?这是因为链栈只需要一个top指针就够操作了,而top指针就是链表的头指针,而一个链表的头指针L在初始化的时候就定死了;但是队列需要尾指针,如果尾指针域定义在节点结构体里,像下面这样:
typedef struct LinkNode{ //定义队列节点 int data; struct LinkNode *next,*rear; //指针域与尾结点域 }LinkNode;
那么除了最后一个节点外的每一个节点都会浪费一个指针域。所以我们需要另起一个LinkQuene结构体来单独区分头尾指针。
基本操作
初始化
void InitLQuene(LinkQuene &Q){
Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkeNode)); //头结点
Q.front->next=nullptr; //置空
}
判队空
bool IsEmpty(LinkQueueQ){
if(Q.front==Q.rear)
return true;
else
return false;
}
入队
void EnLQuene(LinkQuene *Q,int x){
LinkNode *q=(LinkNode*)malloc(sizeof(LinkNode)); //创建新结点
q->data=x;
q->next=nullptr;
Q.rear->next=q; //插入链尾
Q.rear=q; //重置尾指针
}
出队
bool DeQuene(LinkQuene &Q,int &x){
if(Q.rear==Q.front) //判队空
return false;
LinkNode *s=Q.front->next;
x=p->data;
Q.front->next=p->next; //断剑改连
if(Q.rear==p)
Q.rear=Q.front; //如果只有一个元素,重置尾指针
free(p);
return true;
}
这里需要说明一下为什么只有一个元素的时候需要重置尾指针。因为如果只有一个元素,那么尾指针将会指向这个唯一的元素,如果你直接给他free掉了,那么尾指针会变成野指针。
双端队列
双端队列是指允许两端都可以进行插入和删除操作的线性表。双端队列两端的地位是平等的,为了方便理解,将左端也视为前端,右端也视为后端。
有些时候为了某些需求会删除某一端的插入或删除功能,这样的双端队列称为输入受限或者输出受限的双端队列。
队列的注意点
经常会有题目将front指针和rear指针指向的位置胡乱设定(这篇文章使用的都是front指向队首元素,rear指向队尾元素+1),然后问你该队列的判空条件是什么,或者问front与rear的初值是什么。下面给出这种题的通解:
- 假设队列里入队了一个元素。
- 由于入队后rear+1,front不变,倒推队列为空时front和rear的初值。
例题:
1、假设用 A[0..n]实现循环队列,front、rear 分别指向队首元素的前一个位置和队尾元素。若用(rear+1)%(n+1)==front作为队满标志,则()
A.可用front==rear作为队空标志
B.队列中最多可有n+1个元素
C.可用front>rear作为队空标志
D.可用(front+1)号(n+1)==rear作为队空标志
若A里有一个元素,则front=n(0的前一位), rear=0。又因为该元素入队后rear才等于0,所以没入队之前rear=n。故队空时front=rear。选A。
2、已知循环队列存储在一维数组 A[0..n-1]中,且队列非空时 front和 rear 分别指向队头元素和队尾元素。若初始时队列为空,且要求第一个进入队列的元素存储在A[0]处,则初始时front和rear的值分别是()。
A. 0,0
B.0,n-1
C. n-1,0
D.n-1,n-1
若A里有一个元素,则front=0,rear=0。又因为该元素入队后rear才等于0,所以没入队之前rear=n-1。故队空时front=0,rear=n-1。选B。