本文已收录至《数据结构(C/C++语言)》专栏,欢迎大家 👍点赞 + 👆收藏 + 🫰关注 !
目录
前言
前面我们介绍了队列,实现了队列的一些基本操作,这次我们对队列进行拓展,介绍循环队列(环形队列),循环队列如同一个环一样,是封闭的,且空间大小固定,循环队列中最大的问题就是假溢出问题,本章我们将逐一介绍。
正文
循环队列是在队列的基础上,让队列的两端(头尾)相连形成了一种闭环的数据结构,其性质还是队列,但是结果上是一种闭合的循环顺序表或循环链表,所以循环队列的空间大小是固定的,满了就无法再入队了。
循环队列的数据结构
循环队列分为顺序的实现可以由顺序表或者链表去实现,而链表又细分为单链循环队列和双链循环队列。
循环队列中最大的物体就是假溢出问题,以及查看队尾元素时如何处理。
对于效率上的分析,顺序循环队列通过计算可以得到队尾的位置,双链循环队列通过前驱指针可以找到前驱节点,但是单链循环队列找队尾只能用迭代去找,所以这里我们实现顺序循环队列和双链循环队列。
顺序循环队列
//顺序循环队列 typedef int CQCDataType; typedef struct MyCircularQueue { CQCDataType* data;//数据域(数组/顺序表) int front;//头指针 int rear;//尾指针 int cap;//容量 } MyCircularQueue;
双链循环队列
typedef int QCDataType; typedef struct QueueNode { QCDataType data;//数据域 struct QueueNode* next;//后继 struct QueueNode* prev;//前驱 }QueueNode; typedef struct MyCircularQueue { struct QueueNode* front;//头指针 struct QueueNode* rear;//尾指针 } MyCircularQueue;
顺序循环队列的实现
初始化并建立循环队列函数
我们前面介绍过,循环队列的空间大小是固定的,所以我们需要初始化并建立一个顺序队列,这里要注意的是,我们需要多开一个空间辅助我们判断队满防止假溢出,所以实际可用空间是n,实际空间为n+1,因为当尾指针的下一个空间是头指针所指向的空间时表示队满,此时不再插入,而大家可能会问为什么不是头指针等于尾指针就是队满,如果是这样,那么就无法区分队空和队满了,队满也是头指针等于尾指针,所以尾指针必须停下来空出一个空间,这样才能有别于队满的判断。
//初始化并创建顺序循环队列 MyCircularQueue* myCircularQueueCreate(int k) { //开辟一个队列结构体类型 MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue)); if (!obj)//检查是否申请成功 { perror("Qunue Creat Fail!\n"); exit(EOF); } //开辟k个CQCDataType类型空间的顺序表 obj->data = (CQCDataType*)malloc(sizeof(CQCDataType) * (k + 1)); if (!obj->data)//检查是否申请成功 { perror("Array Creat Fail!\n"); exit(EOF); } //置空头尾指针 obj->front = obj->rear = 0; //赋予实际空间大小 obj->cap = k + 1; //返回地址 return obj; }
判断队空函数
顺序循环队列中,当头指针和尾指针在同一个位置时则队空。循环队列的头尾指针可以在任意位置,并非是0才是头。
//判断队空,空返回true bool myCircularQueueIsEmpty(MyCircularQueue* obj) { assert(obj);//判断空指针 return (obj->front) == (obj->rear);//头等于尾则队空 }
判断队满函数
顺序循环队列中,当尾指针指向空间的下一个空间下标对实际空间大小(实际开辟的空间大小)除余等于头指针时,队列就为满。
这里为什么要用下标加1对实际空间大小除余呢?因为当队列走到当前顺序表的最大下标时,需要矫正下标,否则会出现越界!
//判断队满,满返回true bool myCircularQueueIsFull(MyCircularQueue* obj) { //判断空指针 assert(obj); //判断队满,如果rear+1也就是rear的下一个除余的位置等于front那么就是队满 return (obj->rear + 1) % (obj->cap) == (obj->front); }
入队函数
入队函数在入队前需要检查队列是否已满,如果已满则不在插入,否则将数据入队在尾指针所指向的空间,然后尾指针加1并对实际空间大小除余得到尾指针的下一个正确位置。
//入队 void myCircularQueueEnQueue(MyCircularQueue* obj, int value) { //判断空指针 assert(obj); //判断队满 if (!myCircularQueueIsFull(obj)) { //插入rear当前位置后rear后移 (obj->data)[(obj->rear)++] = value; //rear对最大容量除余得到下一个在队列中的位置 (obj->rear) %= (obj->cap); } }
出队函数
出队函数在出队前需要判断队列是否为空,如果为空则不再出队,否则头指针加1后移并对实际空间大小除余得到下一个头指针的位置,这样也是为了否则头指针越界!
//出队 void myCircularQueueDeQueue(MyCircularQueue* obj) { //判断空指针 assert(obj); //判断队空 if (!myCircularQueueIsEmpty(obj)) { //队头后移 ++(obj->front); //front对最大容量除余得到下一个头指针的位置 (obj->front) %= (obj->cap); } }
取队头数据函数
取队头数据非常简单,我们只需要访问头指针下标下的数组数据即可!但是这里需要判断队空,如果队列为空则返回-1(或其他反馈性符号)。
//取头 CQCDataType myCircularQueueFront(MyCircularQueue* obj) { //判断空指针 assert(obj); //判断是否为空队列 if (myCircularQueueIsEmpty(obj)) { return -1;//空队列返回-1 } //返回头指针下标下的数组数值 return obj->data[obj->front]; }
取队尾数据函数
取队尾函数与取队头不同,因为队尾指针在入队数据后会移动到下一个空的空间下,所以我们要求队尾的正确位置,可能有人会说直接减1不就行了,但如果尾指针在0下标,那么减1会越界,此时需要加上实际空间大小再对实际空间大小除于就能得到正确的队尾元素下标!
//取尾 CQCDataType myCircularQueueRear(MyCircularQueue* obj) { //判断空指针 assert(obj); //判断是否为空队列 if (myCircularQueueIsEmpty(obj)) { return -1;//为空返回-1 } 返回队列当前尾指针减1加实际空间对实际空间除余下标下的数组数据 return obj->data[((obj->rear) + (obj->cap) - 1) % (obj->cap)]; }
求队列有效元素个数函数
求队列的有效元素个数只需要用尾指针减去头指针的距离加上实际空间大小对实际空间大小除余即可!
//求有效元素个数 int myCircularQueueNumEff(MyCircularQueue* obj) { assert(obj);//判断空指针 //尾指针减去头指针加上实际空间大小除余即可 return ((obj->rear)-(obj->front) + (obj->cap)) % (obj->cap); }
队列销毁函数
对于顺序队列销毁函数,我们只需要释放顺序表中data指针的内存空间,然后释放我们申请的队列结构体即可!
//销毁队列 void myCircularQueueFree(MyCircularQueue* obj) { assert(obj);//判断空指针 //是否data数组内存空间 free(obj->data); //释放队列结构体内存空间 free(obj); }
双链循环队列的实现
初始化并创建双链循环队列
我们在创建循环队列前需要先申请一个双链循环队列结构体,然后开始搭建循环双链表,这里我们仍然是搭建n个可用空间,n+1个可用空间的双向链表,我们需要定义一个头节点变量方便链表创建完成时进行头尾相连,然后定义一个迭代变量,每次迭代变量将新的节点与自己相连,然后自己走向新节点的地址,不断搭桥,直到创建了n+1个节点,如果创建的是第一个节点则把第一个节点的地址给头节点变量保存,否则就不断向前搭桥,当创建完成时将头尾节点相链接并将队列结构体中的头尾指针指向头节点即可,最后返回头节点的地址。
//初始化队列--创造nk节点的循环链表 MyCircularQueue* myCircularQueueCreate(int k) { MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));//申请一个头尾指针域 if (!obj)//检查malloc是否成功 { perror("malloc CircularQueue fail! \n"); exit(-1); } int i = k + 1;//多申请一个节点方便判断队空和队满 QueueNode* GuardNode = NULL; QueueNode* curNode = NULL; while (i--)//创造k个节点 { QueueNode* Node = (QueueNode*)malloc(sizeof(QueueNode)); if (!Node)//检查malloc是否成功 { perror("malloc QueueNode fail! \n"); exit(-1); } Node->data = 0; Node->next = NULL; Node->prev = NULL; if (!GuardNode)//头节点为空--初始化头节点 { curNode = GuardNode = Node;//头尾节点指针指向第一个节点 } else { curNode->next = Node;//连接新节点 Node->prev = curNode;//新节点指向 curNode = Node;//遍历节点指向下一个节点 } } curNode->next = GuardNode;//头尾相连 GuardNode->prev = curNode; obj->front = obj->rear = GuardNode;//头尾指针指向头 return obj; }
队列判空函数
空队列的判断只需要判断头尾指针是否相等即可!
//判断队空 bool myCircularQueueIsEmpty(MyCircularQueue* obj) { assert(obj); return (obj->front) == (obj->rear);//判断队列空 }
队列判满函数
如果队列已满,则尾指针的下一个一定是头指针,此时队满!
//判断队满 bool myCircularQueueIsFull(MyCircularQueue* obj) { assert(obj); return (obj->rear->next) == (obj->front);//如果队尾的下一个是队头则队满 }
入队函数
入队前需要判断队列是否已满,如果不满则将数值value赋给当前尾指针所在的节点,然后尾指针走向下一个节点!
//入队 void myCircularQueueEnQueue(MyCircularQueue* obj, int value) { assert(obj); if (!myCircularQueueIsFull(obj))//判断队满 { obj->rear->data = value;//给当前节点赋值 obj->rear = obj->rear->next;//尾指针走向下一个节点 } }
出队函数
队列的出队只需要将头指针向后走一步走向下一个节点即可!在出队前需要判断队列是否为空!
//出队 void myCircularQueueDeQueue(MyCircularQueue* obj) { assert(obj); if (!myCircularQueueIsEmpty(obj))//判断队空 { obj->front = obj->front->next;//头指针走向下一个节点 } }
取队头数值函数
取队头数值时需要判断队列是否为空,如果为空返回-1(或其他反馈信号),然后直接返回头指针所指向节点的数值即可。
//取队头 QCDataType myCircularQueueFront(MyCircularQueue* obj) { assert(obj); if (myCircularQueueIsEmpty(obj))//判断队空 return -1;//为空返回-1 return obj->front->data;//不为空返回当前头节点数值 }
取队尾数值函数
取队尾节点数值时也要判断队列是否为空,如果为空返回-1(或其他反馈信号),然后返回尾指针指向节点的前驱节点的数值即可!
//取队尾 QCDataType myCircularQueueRear(MyCircularQueue* obj) { assert(obj); if (myCircularQueueIsEmpty(obj))//判断队空 return -1;//为空返回-1 return obj->rear->prev->data;//返回当前队尾的上一个(目前的队尾) }
求有效元素个数函数
求有效元素函数只需要通过迭代,从头指针一直遍历到尾指针(不包含尾指针)记下节点个数即可。(反之,因为是双链循环队列,如果要求有效空间大小只需要从尾指针遍历到头指针(不包含头指针)记下节点个数即可!)
//求有效元素个数 int myCircularQueueNumEff(MyCircularQueue* obj) { assert(obj);//判断空指针 QueueNode* cur = obj->front;//记录头节点 int num = 0;//计数器 while (cur != obj->rear)//迭代计数节点数 { cur = cur->next; ++num; } return num; }
队列销毁函数
对于队列的销毁,我们采用假队满处理,定义一个指针指向头指针的地址,然后定义一个尾指针从尾节点开始遍历一直找到头节点停止,然后先销毁尾指针的内存空间但是保留尾指针所存放的地址,从头指针开始依次向后销毁一直到被最先销毁的尾节点就结束,最后销毁队列结构体空间即可!
//销毁队列 void myCircularQueueFree(MyCircularQueue* obj) { assert(obj); QueueNode* fcur = obj->front;//取当前的头尾节点 QueueNode* rcur = obj->rear; while (rcur->next != fcur)//手动将fcur和rcur两个头尾指针变成队满状态 { rcur = rcur->next; } free(rcur);//先释放rcur指针的内存但是保留这个位置方便下面的遍历 while (fcur != rcur)//开始遍历释放节点 { QueueNode* freeNode = fcur; fcur = fcur->next; free(freeNode); } free(obj); }
总结
到这里循环队列的介绍就结束了,相信大家了解完循环队列以及其实现的两种结构一定收获满满,可以发现用顺序表和链表实现循环队列有不同的特性和高效之处,循环队列在计算机的操作系统课程中会提到生产者与消费者的关系,所以掌握循环队列也是非常重要的!
本次队循环队列的基础知识介绍就到这里啦,希望能够尽可能帮助到大家。
如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!
博客中的所有代码合集:循环队列博客
🌟其他文章阅读推荐🌟
🌹欢迎读者多多浏览多多支持!🌹