1.队列的逻辑结构 与 抽象数据类型定义
-
先进先出的线性表
在顺序队列中,我们使用头指针front指向队首元素;用尾指针rear指向队尾元素的下一个位置(当然这里的指针是用下标模拟出来的)
同时顺序队列中的元素当然是用数组来存储的 -
抽象数据类型如下:
注意这里的传参,本篇使用指针传参,与图中c++的引用有所区别
2.一般顺序队列的模拟操作
- 初始时,头尾指针均指向下标为0的位置
- 始终遵循先进先出原则;队头出队,队尾入队
3.一般顺序队所面临的假上溢问题:(问题一)
设数组大小为MAXQSIZE
当尾指针rear==MAXQSIZE时 就会出现溢出现象,而这种溢出又分为两种情况
- 1.真溢出:
即数组的所有空间都存在元素
特征为:
(1)头指针 front = 0
(2)尾指针 rear = MAXQSIZE
- 2.假溢出
即数组中还存在空闲空间,但尾指针已经越界
特征为:
(1)头指针 front != 0
(2)尾指针 rear = MAXQSIZE
而为了避免这种假上溢问题,我们就引入了循环队列来解决
4.循环队列解决假上溢问题
4.1将队中元素依次向队头方向移动
但是这样做有一个显著的缺点,即浪费时间;每移动一次,队中元素都要移动。所以并不推荐
4.2循环队列解决假上溢问题
将队空间设想成一个循环的表,即分配给队列的m个存储单元可以循环使用,当rear为maxqsize时,若数组的开始端空着,又可从头使用空着的空间;当front为maxqsize时,同理。
设数组名为data, 则核心思路为:
- data[0]接在data[MAXQSIZE-1]之后
- 即当rear+1==MAXQSIZE时,令rear=0;
要想实现上述顺序的循环操作,就需要一个重要的运算方法,即模运算
- 队尾插入元素时:
Q->data[Q->rear]=e;//将元素插入到当前尾指针指向的空间(即队尾元素的后一个位置)
Q->rear=(Q->rear+1)% MAXQSIZE;//更新队尾指针指向队尾元素的下一个位置
这里取模的作用就是当rear为maxqsize时,若数组的开始端空着,就可以更新尾指针指向空着的开始端空间(详见第2种插入的情况)
-
- 1.一般情况下的插入:
-
-
- 数组存在空闲的开始端 且 在数组的最后一个空间插入时(此时尾指针需要更新到空闲的数组开始端):
-
- 队头删除元素时:
*e=Q->data[Q->front];//将队头元素传递给变量e
Q->front=(Q->front+1)% MAXQSIZE;//更新队头指针指向当前队头的位置
这里取模的作用就是当front为maxqsize时,若数组的开始端空着,就可以更新头指针指向空着的开始端空间(详见第2种删除的情况)
-
- 1.一般情况下的删除:
- 1.一般情况下的删除:
-
- 2.数组存在空闲的开始端 且 且此时头指针指向的空间为数组的最后一个空间(此时头指针需要更新到空闲的数组开始端):
- 2.数组存在空闲的开始端 且 且此时头指针指向的空间为数组的最后一个空间(此时头指针需要更新到空闲的数组开始端):
4.3循环顺序对的 物理结构 与 逻辑结构 的辨析
在解决假上溢问题时,我用一维数组的结构图来分析了具体的操作,而这正是循环顺序对的真实的物理结构,我们仅仅是使用特定的操作使其操作起来如同想象中的头尾相连的环形逻辑结构而已,如下图:
5.循环队列面临的队空 与 队满判断条件一致的问题 (问题二)
通过取模法我们解决了一般队列的假上溢问题,但是,正常的循环顺序对又给我们带来了新的问题,如下图:
通过分析我们可以发现,若每个空间都存储一个队列元素,那么不论是队满还是队空,两种情况的判断条件会完全一致
Q->front==Q->rear
6.解决队空 与 队满判断条件一致的方法:
一般有如下三种解决方案,而本篇使用第3种
6.1循环队列解决队满时判断方法–少用一个元素空间:
这种方式可以区别队空和队满的判断条件:
判断队空(头尾指针重合时,队空):
Q->front==Q->rear//头尾指针指针重合时,队空
判断队满(由于少用一个空间,故头尾指针相邻时,队满):
(Q->rear+1)%MAXQSIZE==Q->front
//见上图:共有两种情况
<1>一般情况,Q->rear+1==Q->front
<2>当尾指针指向数组最后一个空间(rear+1==MAXQSIZE)
头指针指向数组第一个空间时(rear==0),
需要用到取模运算:(Q->rear+1)%MAXQSIZE==Q->front
7.循环顺序队的整队实现(少用一个元素空间)
7.1队列的顺序存储结构
//1.队列的顺序存储结构
typedef struct Queue
{
QElemType * data;//存储元素的动态数组
int front;//头指针,若队列不空,指向队列头元素
int rear;//尾指针,若队列不空,指向队列尾元素的下一个位置
//注意这里的头尾指针并不是指针变量,而是用下标模拟指针功能的int变量
}Queue;
//Queue:用来定义循环顺序对列
一个循环顺序队的存储结构中包含头指针,指向队列头元素;尾指针,指向队列尾元素的下一个位置;存储元素的动态数组这三个部分
7.2循环顺序队的初始化
注意:这里需要传入结构指针,因为要修改结构体中成员进行动态分配内存
bool InitQueue(Queue *q)
7.2.1算法步骤:
(1)为循环顺序对动态分配空间
(2)初始化头尾指针为0,表示队列为空
//1.循环顺序队的初始化
//注意:这里需要传入结构指针,因为要修改结构体中成员进行动态分配内存
bool InitQueue(Queue *q)
{
//[1]为循环顺序对动态分配空间
q->data=(QElemType *)malloc(MAXQSIZE*sizeof(QElemType));
//这里由于数组首地址为一个指针,故可以使用q.data来接受mallocmalloc返回的指针
if (!q->data)
{
return false;//内存分配失败
}
//[2]初始化头尾指针为0,表示队列为空
q->front=q->rear=0;
return true;
}
7.3求循环顺序队的长度
注意:这里不用传入结构指针,因为只计算长度,不改变结构体
int QueueLength(Queue q)
7.3.1算法步骤:
(1)(尾指针 与 头指针作差 再加MAXQSIZE)并取余MAXQSIZE,得到队列长度
//注意这里加MAXQSIZE并取余MAXQSIZE,是防止尾指针在低地址端
7.3.2三种情况下的图解
//2.求循环顺序队的长度
//注意:这里不用传入结构指针,因为只计算长度,不改变结构体
int QueueLength(Queue q)
{
//[1](尾指针 与 头指针作差 再加MAXQSIZE)并取余MAXQSIZE,得到队列长度
//注意这里加MAXQSIZE并取余MAXQSIZE,是防止尾指针在低地址端
return (q.rear - q.front + MAXQSIZE) % MAXQSIZE;
}
7.4循环顺序队的入队操作(核心1)
注意:这里需要传入结构指针,因为要修改结构体中成员:传入元素,修改尾指针
bool EnQueue(Queue * q,QElemType e)
7.4.1算法步骤:
(1)判断队列是否已满
(2)进行入队操作
<1>新元素插入到当前尾指针指向的空间(当前队尾元素的下一个位置)
<2>更新尾指针,指向新的队尾结点的下一个位置
//3.循环顺序队的入队操作
//注意:这里需要传入结构指针,因为要修改结构体中成员:传入元素,修改尾指针
bool EnQueue(Queue * q,QElemType e)
{
//[1]判断队列是否已满
//这里对MAXQSIZE进行取余,是为了应对以下情况
//尾指针指向数组最后一个空间(rear+1==MAXQSIZE),头指针指向数组第一个空间时(rear==0)
//因为 MAXQSIZE % MAXQSIZE == 0
if ((q->rear + 1) % MAXQSIZE == q->front)
{
return false;//队满
}
//[2]进行入队操作
//<1>新元素插入到当前尾指针指向的空间(当前队尾元素的下一个位置)
q->data[q->rear] = e;
//<2>更新尾指针,指向新的队尾结点的下一个位置
//这里对MAXQSIZE进行取余,是为了应对以下情况(防止上溢出)
//尾指针指向数组最后一个空间(rear+1==MAXQSIZE),要更新尾指针指向数组第一个空间时(rear==0)
//因为 MAXQSIZE % MAXQSIZE == 0
q->rear = (q->rear + 1) % MAXQSIZE;
return true;
}
7.5循环顺序队的入队操作(核心2)
注意:这里需要传入结构指针,因为要修改结构体中成员:修改头指针
bool Dequeue(Queue * q,QElemType * e)
7.5.1算法步骤:
(1)判断队列是否为空
(2)保存队头元素
(3)更新头指针,指向新的队头结点
//4.循环顺序队的出队操作
//注意:这里需要传入结构指针,因为要修改结构体中成员:修改头指针
bool Dequeue(Queue * q,QElemType * e)
{
//[1]判断队列是否为空
if (q->rear == q->front)
{
return false;//队空
}
//[2]保存队头元素
*e = q->data[q->front];
//[3]更新头指针,指向新的队头结点
//这里对MAXQSIZE进行取余,是为了应对以下情况(防止上溢出)
//头指针指向数组最后一个空间(front+1==MAXQSIZE),要更新头指针指向数组第一个空间时(front == 0)
//因为 MAXQSIZE % MAXQSIZE == 0
q->front = (q->front + 1) % MAXQSIZE;
return true;
}
7.6循环顺序队的取队头元素操作(核心3)
注意:这里不需要传入结构指针,因为不修改结构体中成员,仅传出元素
bool GetFront(Queue q, QElemType* e)
7.6.1算法步骤:
(1)判断是否队空
(2)取出队头元素
//5.循环顺序队的取队头元素操作
//注意:这里不需要传入结构指针,因为不修改结构体中成员,仅传出元素
bool GetFront(Queue q, QElemType* e)
{
//[1]判断是否队空
if (q.rear == q.front)
{
return false;//队空
}
//[2]取出队头元素
*e = q.data[q.front];
return true;
}
7.7循环顺序队的清空操作
注意:这里需要传入结构指针,因为要修改结构体中成员;头尾指针
bool ClearQueue(Queue* q)
7.7.1算法步骤
(1)直接将头尾指针重置为0
//6.循环顺序队的清空操作
//注意:这里需要传入结构指针,因为要修改结构体中成员;头尾指针
bool ClearQueue(Queue* q)
{
//[1]直接将头尾指针重置为0
//注意:这里是逻辑上的清空,物理上并没有清空队列,只是重置了头尾指针
//在入队新元素时,是可以将原来队列中可能存在的元素覆盖掉的
q->front = q->rear = 0; // 重置头尾指针
return true;
}
7.8循环顺序队的销毁操作
注意:这里需要传入结构指针,因为要修改结构体中成员;头尾指针,并释放数组空间
bool DestroyQueue(Queue* q)
7.8.1算法步骤
(1)直接销毁存储队列元素的动态数组
(2)注意重置头尾指针
//7.循环顺序队的销毁操作
//注意:这里需要传入结构指针,因为要修改结构体中成员;头尾指针,并释放数组空间
bool DestroyQueue(Queue* q)
{
//[1]直接销毁存储队列元素的动态数组
if (q->data)
{
free(q->data); // 释放动态分配的内存
q->data = NULL;
}
//[1]注意重置头尾指针
q->front = q->rear = 0; // 重置指针
return true;
}
7.9遍历队列并打印(从队头开始)
bool TraverseQueue(Queue* q)
7.9.1算法步骤:
(1)初始化计数器等于头指针的值
(2)循环遍历队列元素
注意更新计数器时的取模操作(i+1==MAXQSIZE时,i更新到下标为0处),保证循环结构的正确性
//8.遍历队列并打印(从队头开始)
bool TraverseQueue(Queue* q)
{
//[1]初始化计数器等于头指针的值
int i = q->front;
//[2]循环遍历队列元素
while (i != q->rear)
{
printf("%d ", q->data[i]);
i = (i + 1) % MAXQSIZE;
//注意更新计数器时的取模操作,保证循环结构的正确性
}
printf("\n");
return true;
}
7. 10所有操作汇总:
//循环顺序队列(动态数组,用浪费一个数组空间的方式来解决)
#include<stdio.h>
#include<stdlib.h>
#define MAXQSIZE 100//最大队列长度
#define bool int
#define true 1
#define false 0
typedef int QElemType;
//1.队列的顺序存储结构
typedef struct Queue
{
QElemType * data;//存储元素的动态数组
int front;//头指针,若队列不空,指向队列头元素
int rear;//尾指针,若队列不空,指向队列尾元素的下一个位置
//注意这里的头尾指针并不是指针变量,而是用下标模拟指针功能的int变量
}Queue;
//Queue:用来定义循环顺序对列
//1.循环顺序队的初始化
//注意:这里需要传入结构指针,因为要修改结构体中成员进行动态分配内存
bool InitQueue(Queue *q)
{
//[1]为循环顺序对动态分配空间
q->data=(QElemType *)malloc(MAXQSIZE*sizeof(QElemType));
//这里由于数组首地址为一个指针,故可以使用q.data来接受mallocmalloc返回的指针
if (!q->data)
{
return false;//内存分配失败
}
//[2]初始化头尾指针为0,表示队列为空
q->front=q->rear=0;
return true;
}
//2.求循环顺序队的长度
//注意:这里不用传入结构指针,因为只计算长度,不改变结构体
int QueueLength(Queue q)
{
//[1](尾指针 与 头指针作差 再加MAXQSIZE)并取余MAXQSIZE,得到队列长度
//注意这里加MAXQSIZE并取余MAXQSIZE,是防止尾指针在低地址端
return (q.rear - q.front + MAXQSIZE) % MAXQSIZE;
}
//3.循环顺序队的入队操作
//注意:这里需要传入结构指针,因为要修改结构体中成员:传入元素,修改尾指针
bool EnQueue(Queue * q,QElemType e)
{
//[1]判断队列是否已满
//这里对MAXQSIZE进行取余,是为了应对以下情况
//尾指针指向数组最后一个空间(rear+1==MAXQSIZE),头指针指向数组第一个空间时(rear==0)
//因为 MAXQSIZE % MAXQSIZE == 0
if ((q->rear + 1) % MAXQSIZE == q->front)
{
return false;//队满
}
//[2]进行入队操作
//<1>新元素插入到当前尾指针指向的空间(当前队尾元素的下一个位置)
q->data[q->rear] = e;
//<2>更新尾指针,指向新的队尾结点的下一个位置
//这里对MAXQSIZE进行取余,是为了应对以下情况(防止上溢出)
//尾指针指向数组最后一个空间(rear+1==MAXQSIZE),要更新尾指针指向数组第一个空间时(rear==0)
//因为 MAXQSIZE % MAXQSIZE == 0
q->rear = (q->rear + 1) % MAXQSIZE;
return true;
}
//4.循环顺序队的出队操作
//注意:这里需要传入结构指针,因为要修改结构体中成员:修改头指针
bool Dequeue(Queue * q,QElemType * e)
{
//[1]判断队列是否为空
if (q->rear == q->front)
{
return false;//队空
}
//[2]保存队头元素
*e = q->data[q->front];
//[3]更新头指针,指向新的队头结点
//这里对MAXQSIZE进行取余,是为了应对以下情况(防止上溢出)
//头指针指向数组最后一个空间(front+1==MAXQSIZE),要更新头指针指向数组第一个空间时(front == 0)
//因为 MAXQSIZE % MAXQSIZE == 0
q->front = (q->front + 1) % MAXQSIZE;
return true;
}
//5.循环顺序队的取队头元素操作
//注意:这里不需要传入结构指针,因为不修改结构体中成员,仅传出元素
bool GetFront(Queue q, QElemType* e)
{
//[1]判断是否队空
if (q.rear == q.front)
{
return false;//队空
}
//[2]取出队头元素
*e = q.data[q.front];
return true;
}
//6.循环顺序队的清空操作
//注意:这里需要传入结构指针,因为要修改结构体中成员;头尾指针
bool ClearQueue(Queue* q)
{
//[1]直接将头尾指针重置为0
//注意:这里是逻辑上的清空,物理上并没有清空队列,只是重置了头尾指针
//在入队新元素时,是可以将原来队列中可能存在的元素覆盖掉的
q->front = q->rear = 0; // 重置头尾指针
return true;
}
//7.循环顺序队的销毁操作
//注意:这里需要传入结构指针,因为要修改结构体中成员;头尾指针,并释放数组空间
bool DestroyQueue(Queue* q)
{
//[1]直接销毁存储队列元素的动态数组
if (q->data)
{
free(q->data); // 释放动态分配的内存
q->data = NULL;
}
//[1]注意重置头尾指针
q->front = q->rear = 0; // 重置指针
return true;
}
//8.遍历队列并打印(从队头开始)
bool TraverseQueue(Queue* q)
{
//[1]初始化计数器等于头指针的值
int i = q->front;
//[2]循环遍历队列元素
while (i != q->rear)
{
printf("%d ", q->data[i]);
i = (i + 1) % MAXQSIZE;
//注意更新计数器时的取模操作,保证循环结构的正确性
}
printf("\n");
return true;
}
// 主函数测试代码
int main()
{
Queue q;
// 初始化队列
if (InitQueue(&q)) {
printf("初始化队列成功!\n");
}
// 入队操作
for (int i = 1; i <= 10; i++)
{
EnQueue(&q, i);
}
// 遍历队列
printf("队列中的元素: ");
TraverseQueue(&q);
// 出队操作
int e;
Dequeue(&q, &e);
printf("出队元素: %d\n", e);
// 再次遍历队列
printf("队列中的元素: ");
TraverseQueue(&q);
// 取队头元素
GetFront(q, &e);
printf("队头元素: %d\n", e);
// 清空队列
ClearQueue(&q);
printf("队列清空后长度: %d\n", QueueLength(q));
// 销毁队列
DestroyQueue(&q);
return 0;
}