目录
一、基本概念
1.定义:
只允许在一端进行插入(入队),在另一端删除(出队)的线性表;
2.特点:
先进先出(FIFO:First In First Out);
3.相关术语:
(1)空队列:没有元素的队列称为空队列;
(2)队尾:允许插入的一端,队列中最靠近队尾的元素称为队尾元素;
(3)队头:允许删除的一端,队列中最靠近队头的元素称为队头元素;
4.基本操作:
(1)InitQueue(&Q),初始化队列:构造一个空队列Q;
(2)DestroyQueue(&Q),销毁队列:销毁并释放队列Q所占用的内存空间;
(3)EnQueue(&Q,x),入队:若队列Q未满,将x加入,使之成为新的队尾;
(4)DeQueue(&Q,&x),出队:若队列Q非空,删除队头元素,并用x值返回;
(5)GetHead(Q,&x),读队头元素:若队列Q非空,则将队头元素赋值给x;
(6)QueueEmpty(Q),队列判空:若队列Q为空返回true,否则返回false。
二、队列的顺序实现
1.结构定义
#define MaxSize 10 //定义队列中元素的最大个数
typedef struct {
int data[MaxSize]; //用静态数组存放队列元素
int front, rear; //队头指针和队尾指针
}SqQueue;
(1)声明时:SqQueue Q;
(2)front指向队头元素;rear指向队尾元素的后一个位置(下一个应该插入的位置);
(3)这里的front和rear也同样不是数据类型指针,而是功能上类似指针的int型整数,在这里映射的是队头队尾元素的数组下标。
2.初始化
(1)InitQueue(&Q),初始化队列:构造一个空队列Q;代码如下:
void InitQueue(SqQueue& Q)
{
Q.rear = 0;
Q.front = 0;
}
(2)初始化时令两个指针都指向0的位置即可。
3.判空
(1)基于初始化函数中给出的构造空队列的方法,则判空条件为front=rear:
bool QueueEmpty(SqQueue Q)
{
if (Q.front = Q.rear) //队空条件
return true;
else
return false;
}
4.入队
(1)EnQueue(&Q,x),入队:若队列Q未满,将x加入,使之成为新的队尾;代码如下:
bool EnQueue(SqQueue& Q, int x)
{
if ((Q.rear + 1) % MaxSize == Q.front) //队满报错,条件在下面解释中提出
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear + 1) % MaxSize; //A
return true;
}
(2)对注释中A句的补充解释:
如果队尾插满后,rear应该指向的是data[9]的下一个位置也就是指向了rear的存储空间,此时rear=10;如果现在队头有元素出队,那么仅凭一个rear=MaxSize并不能判断队列存满;
这时(尾部满了头部走了),如果还要插入元素的话,就只能在队头插入。所以仅仅让rear自增1的操作不能满足我们在队头插入的需求,于是用到取余运算。
这样一来,模运算将无限的整数域映射到有限的整数集合上,将存储空间在逻辑上变成了“环状”;用这种方式实现的队列称为“循环队列”(用模运算将存储空间在逻辑上变成了“环状”)。
再来说队满的条件:按理来说,rear和front指向同一个存储空间时,队列真正存满,但这时会存在一个问题;前面我们在判空函数中说到,rear和front一旦指向同一个位置,就意味着队列判空,所以队满的条件不能这样安排;我们不得已需要牺牲一个存储单元来做到判断队满,条件如下:
队尾指针的再下一个位置是队头,即(Q.rear+1)%MaxSize==Q.front。
5.出队
(1)DeQueue(&Q,&x),出队:若队列Q非空,删除队头元素,并用x值返回;代码如下:
bool DeQueue(SqQueue& Q, int& x)
{
if (Q.front == Q.rear) //空队列报错
return false;
x = Q.data[Q.front]; //将队头指针所指的元素的数据域赋值给x
Q.front = (Q.front + 1) % MaxSize; //队头指针后移
return true;
}
6.查队头元素
(1)GetHead(Q,&x),读队头元素:若队列Q非空,则将队头元素赋值给x;代码如下:
bool GetHead(SqQueue Q, int& x)
{
if (Q.front == Q.rear) //空队列报错
return false;
x = Q.data[Q.front]; //将队头指针所指的元素的数据域赋值给x
return true;
}
7.相关说明:
(1)队列元素个数计算:(rear+MaxSize-front)%MaxSize;
(2)上面提到了一种判满判空的方法,但是需要浪费一个存储单元,下面给出两种方法可以避免这种浪费:
①初始化时,在队列结构中再定义一个int size来表示当前队列长度,每次插入成功size++,每次删除成功size--;则判满条件为size==MaxSize;判空条件为size==0;
②初始化时,在队列结构中再定义一个int tag来表示最近一次执行的是插入还是删除;每次插入成功后把tag=1,每次删除成功后把tag=0;则判满条件为Q.rear==Q.front&&tag==1;判空条件为Q.rear==Q.front&&tag==0;
(3)有时题目让rear指向当前的队尾元素而不是队尾元素的下一个位置,这时相对应的一些操作改变如下:
①入队:Q.rear=(Q.rear+1)%MaxSize; Q.data[Q.rear]=x;
②初始化:队尾指针指向MaxSize-1,即Q.rear=MaxSize-1;
③判空:(Q.rear+1)%MaxSize==Q.front;
④判满:不加辅助变量的前提下,也就是牺牲一个存储单元,那rear就只能指向front前两个位置,也就是(Q.rear+2)%MaxSize==Q.front。
三、队列的链式实现
1.链队列定义
typedef struct LinkNode { //链式队列结点
int data;
struct LinkNode* next;
}LinkNode;
typedef struct { //链式队列
LinkNode* front, * rear; //队列的队头和队尾指针
}LinkQueue;
2.初始化
(1)带头结点
void InitQueue(LinkQueue& Q)
{
Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
Q.front->next = NULL;
}
(2)无头结点
void Initqueue(LinkQueue& Q)
{
Q.front = NULL;
Q.rear = NULL;
}
3.判空
(1)带头结点
bool IsEmpty(LinkQueue Q)
{
if (Q.front == Q.rear)
return true;
else
return false;
}
(2)无头结点
bool Isempty(LinkQueue Q)
{
if (Q.front == NULL)
return true;
else
return false;
}
4.入队
(1)带头结点
void EnQueue(LinkQueue& Q, int x)
{
LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
Q.rear->next = s; //新结点插入到rear之后
Q.rear = s; //修改表尾指针
}
(2)无头结点
void Enqueue(LinkQueue& Q, int x)
{
LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
if (Q.front == NULL) //在空队列中插入第一个元素
{
Q.front = s; //修改队头队尾指针
Q.rear = s; //不带头结点的队列,第一个元素入队需要特殊处理
}
else {
Q.rear->next = s; //新结点插入到rear结点之后
Q.rear = s; //修改rear指针
}
}
5.出队
(1)带头结点
bool DeQueue(LinkQueue& Q, int& x)
{
if (Q.front == Q.rear)
return false; //空队列
LinkNode* p = Q.front->next; //定义p指向要删除的结点,也就是头结点的下一个结点
x = p->data; //用变量x返回要删除的元素
Q.front->next = p->next; //修改头结点的next指针
if (Q.rear == p) //此次是最后一个结点出队
Q.rear = Q.front; //修改rear指针
free(p);
return true;
}
(2)无头结点
bool Dequeue(LinkQueue& Q, int& x)
{
if (Q.front = NULL)
return false; //空队,没有可出元素
LinkNode* p = Q.front; //定义p指向头指针所指的结点,也就是出队的结点
x = p->data; //用变量x返回队头元素
Q.front = p->next; //修改front指针
if (Q.rear == p) //此次是最后一个结点出队
{
Q.front = NULL;
Q.rear = NULL; //修改front和rear指针都指向空
}
free(p); //释放结点空间
return true;
}
6.补充说明
(1)顺序存储时预分配的空间耗尽则队满,而在链式存储时一般不会出现队满的情况,除非内存不足。
(2)双端队列
①概念:允许从两端插入、两端删除的线性表,若只使用一端的插入删除操作,则效果等同于栈;
②输入受限的双端队列:只允许从一端插入、两端删除的线性表;
③输出受限的双端队列:只允许从两端插入、一端删除的线性表。