一、什么是循环队列
1.队列
(1)队列是只允许在一端 进行插入操作,而在另一端进行删除操作的线性表。
(2)特点:先进先出 (FIFO:First In First Out)
(3)允许插入的一端称为队尾 ,允许删除的一端称为队头。

(4)同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行 ,删除数据只能在队头进行。
队列的插入操作,一般称为入队 ;队列的删除操作,一般称为出队。
2.循环队列
(1)队列的顺序存储结构
线性表有顺序存储和链式存储,队列作为一种特殊的线性表,同样存在这两种存储方式。
队列的链式存储结构 :链队列
数据结构------链队列的设计及函数实现(C语言)-CSDN博客
https://blog.csdn.net/wy_05136/article/details/160090401?spm=1001.2014.3001.5501队列的顺序存储结构 :有一组地址连续的存储单元依次存放从队头到队尾的元素。

我们假设一个队列有n个元素,则顺序存储的队列需建立一个大于n的数组,并把队列的所有元素存储在数据的前n个单位,数组的下标为0的一端即是队头。
(2)设计顺序存储的队列------循环队列
对于设计顺序存储的队列,我们需要解决4个问题:
(1)所谓的入队操作,其实就是在队尾追加一个元素,不需要移动任何元素,因此时间复杂度为O(1);但是,出队操作是删除队头元素,这意味着,队列中的所有元素都要向前移动一位,此时时间复杂度为O(n)。


问题一:如何对顺序表进行改造,让其插入和删除的时间复杂度都达到O(1)?
解决方法 :我们设置两个指针front 和rear分别指示队头元素和队尾元素的位置。
注意 :我们通常默认front指针 指向第一个元素的位置;rear指针 指向最后一个元素的下一个位置。
当队列为空 时,front == rear。
这样,当我们进行出队操作时,我们不用移动队列中的元素,只需要将front指针往后移动一位,把下一个元素当作队头即可。

(2)我们假设一个长度为5的数组,其中存放一个元素个数为4的队列,front指针指向第一个元素a1,rear指针指向最后一个元素a4的下一个位置。
首先,我们进行出队操作,rear指针不变,front指针往后移动一位,指向a2;
接着,我们进行入队操作,front指针不变,rear指针往后移动一位。
此时我们发现,rear指针移动到了数组之外,会造成内存泄露;
此外,此时数组末尾已经没有空闲位置,如果再次进行入队操作的话,就会产生数组越界 的错误,但是,实际上我们的队列在0下标的位置还是空闲的,这个数组我们并没有充分利用,造成内存浪费。我们把这种现象叫做"假溢出"。

问题二:我们如果将出队时所留下的空闲位置再次利用上?
解决方法 :当末尾满了,我们让它再从头开始,也就是让队列头尾相接。
我们把队列的这种头尾相接的顺序存储结构 称为循环队列。
如何实现让rear指针从头开始?
rear = (rear+1) % MAXSIZE;
MAXSIZE:队列的最大长度
此方法,虽然可以可以将空闲位置利用上,但是,也意味着我们无法对数组进行扩容处理;
所以,当我们选择使用循环队列是,必须为它设定一个最大队列长度;
若用户无法预估所用队列的最大长度,则最好采用链队列。
(3)继续上述的例子,我们入队a5,front指针不变,rear指针指向0下标位置(rear = (4+1) % 5 = 0);
接着,我们入队a6,front指针不变,rear指针往后移动一位(rear = (0+1) % 5 = 1)。
我们前面提到,空队列时,front == rear;
此时,队列满时,我们发现 front == rear也成立。

问题三:由于队列头尾相连,导致判空和判满条件冲突,如何区分开?
解决方法一:加一个成员变量Size,用于保存当前元素个数;Size == 0时,队列为空,Size != 0 && rear == front时,队列为满。
解决方法二(常用) :我们在队列尾部保留一个元素空间。这样的话,当数组中还剩一个空闲位置,此时我们就认为队列已满。判满条件:rear指针再走一步就遇到front指针( (rear+1) % MAXSIZE == front)。
也就是说,当出现下图情况时( (rear+1) % MAXSIZE == (0+1) % 5 == 1 == front),我们就认为队列已满,不再进行入队操作。

(4)我们发现,当rear > front时,队列的长度为rear-front;
但当rear < front时,队列的长度分为两段,一段是MAXSIZE-front,另一端是rear-0,加在一起,队列的长度为rear-front+MAXSIZE

问题四:如何用一个通用公式求队列的有效长度?
解决方法:Size = (rear-front+MAXSIZE) % MAXSIZE
二、循环队列结构体设计及函数实现(C语言)
(一)循环队列结构体设计
1.用一组地址连续的存储单元(通常用数组):依次存放从队头到队尾的元素
2.front指针:指向队头元素的位置
3.rear指针:指向队尾元素的下一个位置

cpp
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#define MAXQSIZE 10
typedef int ElemType;
//循环队列的结构体设计
typedef struct CircleQueue {
//1.指针类型:用来接收malloc的返回值
ElemType* arr;
//2.队头指针,指向第一个元素
int front;
//3.队尾指针,指向最后一个元素的下一个位置
int rear;
}CircleQueue;
(二)链队列的C语言函数实现
1.初始化

cpp
void Init_CircleQueue(CircleQueue* pcq) {
//0.断言:检查传入的指针是否为空
assert(pcq != NULL);
//1.动态分配队列的底层数组内存
pcq->arr = (ElemType*)malloc(MAXQSIZE * sizeof(ElemType));
if (pcq->arr == NULL) exit(EXIT_FAILURE); //内存分配失败
//2.front == rear == 0,表示队列为空
pcq->front = 0;
pcq->rear = 0;
}
2.判空
cpp
bool Empty(CircleQueue* pcq) {
//0.断言:检查传入的指针是否为空
assert(pcq != NULL);
//1.若front == rear,说明队列为空,返回true;
// 否则,返回false
return pcq->front == pcq->rear;
}
3.判满

cpp
bool Full(CircleQueue* pcq) {
//0.断言:检查传入的指针是否为空
assert(pcq != NULL);
//1.若rear指针再走一步就遇到front指针,说明队列已满,返回true;
// 否则,返回false
return (pcq->rear + 1) % MAXQSIZE == pcq->front;
}
4.入队

cpp
bool Push(CircleQueue* pcq, ElemType val) {
//0.断言:检查传入的指针是否为空
assert(pcq != NULL);
//1.判满(如果满了,则无法进行入队操作,直接返回)
if (Full(pcq)) {
printf("队列已满,无法入队\n");
return true;
}
//2.直接给rear下标的位置插入val
pcq->arr[pcq->rear] = val;
//3.rear指针往后挪动一位
pcq->rear = (pcq->rear + 1) % MAXQSIZE;
return true;
}
5.出队

cpp
bool Pop(CircleQueue* pcq) {
//0.断言:检查传入的指针是否为空
assert(pcq != NULL);
//1.判空
assert(!Empty(pcq));
//2.直接将front指针往后挪动一位
pcq->front = (pcq->front + 1) % MAXQSIZE;
return true;
}
6.获取队头元素值

cpp
ElemType Front(CircleQueue* pcq) {
//0.断言:检查传入的指针是否为空
assert(pcq != NULL);
//1.判空,若队列为空,则不存在队头元素
assert(!Empty(pcq));
//2.直接返回队头元素的值
return pcq->arr[pcq->front];
}
7.获取队列有效元素个数
详细解释可见前面的问题四。
cpp
int Get_Size(CircleQueue* pcq) {
//0.断言:检查传入的指针是否为空
assert(pcq != NULL);
return (pcq->rear - pcq->front + MAXQSIZE) % MAXQSIZE;
}
8.打印
cpp
void Show(CircleQueue* pcq) {
//0.断言:检查传入的指针是否为空
assert(pcq != NULL);
//1.从front指针开始,到rear指针结束,依次打印
for (int i = pcq->front; i != pcq->rear; i = (i + 1) % MAXQSIZE) printf("%d ", pcq->arr[i]);
printf("\n");
}
9.清空
cpp
void Clear(CircleQueue* pcq) {
//0.断言:检查传入的指针是否为空
assert(pcq != NULL);
//1.直接将队头指针和队尾指针设为0
pcq->front = 0;
pcq->rear = 0;
}
10.销毁
cpp
void Destroy(CircleQueue* pcq) {
//0.断言:检查传入的指针是否为空
assert(pcq != NULL);
//1.释放内存
free(pcq->arr);
pcq->arr = NULL; //避免野指针
//2.重置状态
pcq->front = 0;
pcq->rear = 0;
}
11.主函数测试
cpp
int main() {
CircleQueue queue;
//初始化
Init_CircleQueue(&queue);
//判空
if (Empty(&queue)) printf("队列为空\n\n");
else printf("队列未空\n\n");
//入队+判满
printf("入队(队列未满):\n");
Push(&queue, 1);
Push(&queue, 2);
Push(&queue, 3);
Push(&queue, 4);
Push(&queue, 5);
Push(&queue, 6);
Push(&queue, 7);
Push(&queue, 8);
Push(&queue, 9);
Show(&queue);
printf("\n");
printf("入队(队列已满):\n");
Push(&queue, 10);
Show(&queue);
printf("\n");
//出队
printf("出队:\n");
Pop(&queue);
Show(&queue);
printf("\n");
//获取队头元素值
printf("队头元素值为 %d\n", Front(&queue));
printf("\n");
//获取队列有效元素个数
printf("队列有效元素个数为 %d\n", Get_Size(&queue));
printf("\n");
//清空+判空
if (Empty(&queue)) printf("队列为空\n\n");
else printf("队列未空\n\n");
Clear(&queue);
if (Empty(&queue)) printf("队列为空\n\n");
else printf("队列未空\n\n");
//销毁
Destroy(&queue);
return 0;
}
