在上一篇文章中,我们学习了队列的基本操作,那么今天这篇文章将介绍 循环队列
我们知道,队列的特性是 先进先出,限定 插入 操作只能在 队尾 进行,而 删除 操作只能在 队头 进行。
循环队列 是一种线性数据结构,其操作表现基于 先进先出 原则并且 队尾被连接在队首之后以形成一个循环。它也被称为 “环形缓冲器”。
1、当 head == tail
时,就是一个 空的循环队列 (如下图所示)
2、当 tail + 1 == head
时,就是一个 满的循环队列 (如下图所示)
注意:
(1)为了避免 空 和 满 混淆,无法区分,那么可以 多开一个空间。
(2)当head == tail
时,就为 空。
(3)当 tail 的下一个位置是 head 时,就是满。
为什么要多开一个空间?
假设我们不多开一个空间,那么当 tail 在最后一个空间插入元素时(如下图所示)。
此时 tail 就向后移动到 head 的位置(如下图所示)。
那么此时不就和 判断空的循环队列 条件一样了吗?
所以,我们的 tail 永远是指向 最后一个元素的下一个位置。
提示:判断队列是否满了,还可以写成(tail+1)%5 == front
。
那么循环队列是用 链表 实现?还是 数组 实现呢?
如果用链表实现的话,不容易找 尾 ,所以我们用数组实现(如下图所示)。
代码示例
typedef int CQDataType;
typedef struct CircularQueue
{
CQDataType* a; //队列
int head; //头
int tail; //尾
int size; //存储数据个数
}CQ;
然后,我们对创建好的 环形队列 进行初始化
代码示例
//环形队列初始化,设置队列长度为 k
void CQueueInit(CQ* pcq, int k) {
assert(pcq);
pcq->a = (int*)malloc(sizeof(int) * (k + 1));
pcq->head = pcq->tail = 0;
pcq->size = k;
}
向循环队列插入一个元素,很简单直接插入即可。
但是有一点要注意,如果 tail 等于 size,那么说明 tail 已经走到边界了,所以 tail 需要置为 0 ,也就是重新返回到 初始位置(如下图所示)。
代码示例
//入队列
void CQueuePush(CQ* pcq, CQDataType x) {
assert(pcq);
/*如果队列满了,那么肯定不能插入*/
if (CQueueFull(pcq)) {
printf("队满\n");
exit(0);
}
/*当程序走到这里说明队列还没有满*/
pcq->a[pcq->tail] = x; // 把x存到tail指向的位置
/*如果队尾指针tail等于size,说明tail已经到边界了*/
if (pcq->tail == pcq->size) {
pcq->tail = 0; // 把tail指向0的位置
}
else { /*如果tail不等于size,那么tail就向后挪动*/
pcq->tail++;
}
}
从循环队列中删除一个元素。
在 循环队列 不为空的时候,直接把 head 向后挪动(如下图所示)。
注意,和 入队列 一样,当 head 等于 size 时, head 需要置为 0 ,也就是重新返回到 初始位置(如下图所示)。
代码示例
//出队列
void CQueuePop(CQ* pcq) {
assert(pcq);
/*如果队列为空, 那么肯定就不能删除*/
if (CQueueEmpty(pcq)) {
printf("队空\n");
exit(0);
}
/*如果head等于size,那么head置为0*/
if (pcq->head == pcq->size) {
pcq->head = 0;
}
else { /*否则head就往后挪动*/
pcq->head++;
}
}
从队首获取元素。
这个很简单,直接返回 队头 的数据即可,但是要注意 队列不能为空。
代码示例
//获取队头元素
CQDataType CQueueFront(CQ* pcq) {
assert(pcq);
/*如果队列为空,那么直接返回*/
if (CQueueEmpty(pcq)) {
printf("队空\n");
exit(0);
}
/*如果不为空,直接返回head的值*/
return pcq->a[pcq->head];
}
获取队尾元素,如果 队列 为空,那么没有数据可以取到。
这里获取 队尾 数据有两种情况。
(1)如果 tail 在 0 的位置,那么队尾数据在 tail 的上一个位置,也就是 size 的位置(如下图所示)
(2)如果 tail 不在 0 的位置,那么队尾数据在 tail 的上一个位置,也就是 tail - 1 的位置(如下图所示)
代码示例
//获取队尾元素
CQDataType CQueueBack(CQ* pcq) {
assert(pcq);
/*如果队列为空,那么直接返回*/
if (CQueueEmpty(pcq)) {
printf("队空\n");
exit(0);
}
/*队尾有两种存在的方式*/
if (pcq->tail == 0) {
return pcq->a[pcq->size]; //如果tail在0的位置,那么tail的上一个位置应该是k
}
else {
return pcq->a[pcq->tail - 1]; //如果tail不在0的位置,那么直接返回tail的上一个位置
}
}
如果 head 等于 tail,说明 队列为空(如下图所示)。
代码示例
//检测队列是否为空
bool CQueueEmpty(CQ* pcq) {
assert(pcq);
return pcq->head == pcq->tail; // 当head和tail指向同一个位置时,说明为空
}
检查循环队列是否已满,这里要分两种情况。
当 tail 在最后一个位置(空),head 在第一个位置时,说明满了(如下图所示)
当 tail 的下一个位置为 head 时,说明满了(如下图所示)
代码示例
//检测队列是否满了
bool CQueueFull(CQ* pcq) {
assert(pcq);
//1.当tail在最后一个位置(空),head在第一个位置,说明满了
if (pcq->tail == pcq->size && pcq->head == 0) {
return true;
}
else { //2.tail的下一个位置为head,说明满了
return pcq->tail + 1 == pcq->head;
}
}
这里需要 free 两次,一次是 结构体,一次是 数组。
代码示例
//销毁队列
void CQueueDestroy(CQ* pcq) {
assert(pcq);
/*要free两次*/
free(pcq->a); // 释放结构体
free(pcq); // 释放结构体里面的数组
}
其实 环形队列 还是很简单的,在实现整个过程时,始终要记住以下几点。
循环队列中,tail 永远指向 最后一个元素的下一个位置。
循环队列中,永远要 多开一个空间,假设需要存储的元素有 5 个,那么需要开辟 6 个空间。
本篇文章中的循环队列是用 数组 实现的,又因为数组的下标是从 0 开始的,所以最后一个空间的下标是 5。
最后附上一张完整的 双向链表 的 接口函数图