zl程序教程

您现在的位置是:首页 >  后端

当前栏目

C/C++数据结构(六) —— 循环队列

2023-09-27 14:19:51 时间

在这里插入图片描述


前言

在上一篇文章中,我们学习了队列的基本操作,那么今天这篇文章将介绍 循环队列

什么是循环队列

我们知道,队列的特性是 先进先出,限定 插入 操作只能在 队尾 进行,而 删除 操作只能在 队头 进行。

循环队列 是一种线性数据结构,其操作表现基于 先进先出 原则并且 队尾被连接在队首之后以形成一个循环。它也被称为 “环形缓冲器”

1、当 head == tail 时,就是一个 空的循环队列 (如下图所示👇)
在这里插入图片描述

2、当 tail + 1 == head 时,就是一个 满的循环队列 (如下图所示👇)
在这里插入图片描述

注意:

(1)为了避免 混淆,无法区分,那么可以 多开一个空间
 
(2)当 head == tail 时,就为
 
(3)当 tail 的下一个位置是 head 时,就是满。

为什么要多开一个空间?

假设我们不多开一个空间,那么当 tail 在最后一个空间插入元素时(如下图所示👇)。
 
在这里插入图片描述
 
此时 tail 就向后移动到 head 的位置(如下图所示👇)。
 
在这里插入图片描述
 
那么此时不就和 判断空的循环队列 条件一样了吗?
 
所以,我们的 tail 永远是指向 最后一个元素的下一个位置
 
提示:判断队列是否满了,还可以写成 (tail+1)%5 == front

1. 初始化队列

那么循环队列是用 链表 实现?还是 数组 实现呢?

如果用链表实现的话,不容易找 ,所以我们用数组实现(如下图所示👇)。
在这里插入图片描述

📝 代码示例

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;
}

2. 入队列

向循环队列插入一个元素,很简单直接插入即可。

但是有一点要注意,如果 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++;
	}
}

3. 出队列

从循环队列中删除一个元素。

循环队列 不为空的时候,直接把 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++;
	}
}

4. 获取队头元素

从队首获取元素。

这个很简单,直接返回 队头 的数据即可,但是要注意 队列不能为空

📝 代码示例

//获取队头元素
CQDataType CQueueFront(CQ* pcq) {
	assert(pcq);

	/*如果队列为空,那么直接返回*/
	if (CQueueEmpty(pcq)) {
		printf("队空\n");
		exit(0);
	}

	/*如果不为空,直接返回head的值*/
	return pcq->a[pcq->head];
}

5. 获取队尾元素

获取队尾元素,如果 队列 为空,那么没有数据可以取到。

这里获取 队尾 数据有两种情况。

(1)如果 tail0 的位置,那么队尾数据在 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的上一个位置
	}
}

6. 检测队列是否为空

如果 head 等于 tail,说明 队列为空(如下图所示👇)。
在这里插入图片描述

📝 代码示例

//检测队列是否为空
bool CQueueEmpty(CQ* pcq) {
	assert(pcq);

	return pcq->head == pcq->tail; // 当head和tail指向同一个位置时,说明为空
}

7. 检测队列是否满了

检查循环队列是否已满,这里要分两种情况。

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;
	}
}

8. 销毁队列

这里需要 free 两次,一次是 结构体,一次是 数组

📝 代码示例

//销毁队列
void CQueueDestroy(CQ* pcq) {
	assert(pcq);

	/*要free两次*/
	free(pcq->a); // 释放结构体
	free(pcq); // 释放结构体里面的数组
}

9. 总结

其实 环形队列 还是很简单的,在实现整个过程时,始终要记住以下几点。

循环队列中,tail 永远指向 最后一个元素的下一个位置
 
循环队列中,永远要 多开一个空间,假设需要存储的元素有 5 个,那么需要开辟 6 个空间。
 
本篇文章中的循环队列是用 数组 实现的,又因为数组的下标是从 0 开始的,所以最后一个空间的下标是 5

接口函数贴图

最后附上一张完整的 双向链表接口函数图

在这里插入图片描述