循环队列(顺序存储)完整解析与实现(数据结构专栏版)
编写的这份代码完美实现了循环队列(环形队列)的核心功能,包括队列初始化、判空、入队、出队、打印等操作,采用顺序存储(数组)实现,符合数据结构中队列的 "先进先出(FIFO)" 特性,且通过取模运算解决了普通顺序队列的 "假溢出" 问题,是一份优秀的循环队列入门实现。下面结合数据结构专栏知识点,从核心结构、原理解析、功能拆解、运行结果、扩展知识点五个方面进行全面讲解。
一、循环队列核心结构解析(数据结构基础)
1. 宏定义与队列结构体定义
c
运行
#define MAX 100 // 队列最大容量
typedef int DATATYPE; // 队列数据类型别名,便于后续修改数据类型
typedef struct QUEUE
{
DATATYPE elem[MAX]; // 顺序存储数组,存放队列元素
int front; // 队头指针:指向队列的第一个有效元素
int rear; // 队尾指针:指向队列的下一个空闲位置(尾后)
}QUEUE;
- 数据结构知识点:循环队列采用顺序存储结构(静态数组),借助数组实现元素的连续存储,访问效率高(O (1));
- 核心设计(循环队列标识):
front和rear不再是单纯的递增指针,而是通过% MAX实现 "循环",解决普通顺序队列的 "假溢出"(数组前面有空闲空间,却因rear到达数组末尾无法入队); - 容量说明:
MAX为数组长度,但循环队列的实际可用容量为MAX-1,这是因为采用(rear+1)%MAX == front作为判满条件,需要预留一个空闲位置区分 "队满" 和 "队空"(二者均满足front == rear,不预留会无法区分); - 可扩展性设计:使用
typedef int DATATYPE定义数据类型别名,后续如需将队列存储字符、结构体等数据,仅需修改该宏定义,无需改动核心逻辑。
2. 队列初始化函数(InitQueue)
c
运行
QUEUE* InitQueue()
{
// 1. 为队列结构体分配堆内存
QUEUE *head=(QUEUE*)malloc(sizeof(QUEUE));
// 注:可补充内存分配失败判断,提升健壮性
// if(head == NULL) { printf("内存分配失败!\n"); exit(0); }
// 2. 队头、队尾指针初始化为0,标识空队列
head->front=head->rear=0;
return head;
}
- 核心功能:创建并初始化一个空的循环队列,返回队列结构体指针;
- 初始化规则:空队列的
front和rear指针相等,且均为 0,这是循环队列判空的核心依据; - 内存说明:队列结构体(包含指针
front/rear和数组elem)分配在堆上,避免栈内存随函数调用结束释放,可在整个程序生命周期内使用。
二、循环队列核心原理(关键知识点)
1. 核心解决的问题:普通顺序队列的 "假溢出"
普通顺序队列中,front和rear仅单向递增,当rear到达数组末尾(rear == MAX-1)时,即使数组前面有空闲空间(因元素出队导致front后移),也无法继续入队,这就是 "假溢出"。
循环队列通过取模运算% MAX ,让front和rear指针到达数组末尾后,能够 "绕回" 数组起始位置,充分利用数组的空闲空间,解决假溢出问题。
2. 两个核心判断条件(循环队列的灵魂)
(1) 判空条件:front == rear
c
运行
int IfEmpty(QUEUE *head)
{
// 队头指针与队尾指针相等,标识队列为空
if(head->front==head->rear)
{
return 1; // 空队列返回1
}
return 0; // 非空返回0
}
- 原理:无论队列如何入队、出队,只要所有元素都被出队,
front最终会与rear重合,标识队列无有效元素; - 注意点:该条件与普通顺序队列判空条件一致,但循环队列中
front和rear可循环移动,含义更丰富。
(2) 判满条件:(rear + 1) % MAX == front
c
运行
int InQueue(QUEUE *head,int num)
{
// 循环判满:预留一个空闲位置,区分队满与队空
if((head->rear+1)%MAX==head->front)
{
printf("Queue is full!\n");
return 0; // 入队失败返回0
}
// 后续入队逻辑...
return 1;
}
- 原理:通过预留一个空闲位置(
rear指向的位置始终空闲),让队满时front与rear不重合,从而与队空条件(front == rear)区分开; - 为什么不直接用
rear == front判满?若不预留空闲位置,队满和队空的条件均为front == rear,程序无法区分两种状态,会导致逻辑混乱; - 容量说明:数组长度为
MAX,实际可存储的元素个数为MAX-1,这是循环队列顺序存储实现的一个小代价。
3. 循环队列的 "循环" 实现:取模运算
循环队列的入队、出队、打印操作中,均通过% MAX实现指针的循环移动,核心代码如下:
- 入队时
rear指针更新:head->rear = (head->rear + 1) % MAX; - 出队时
front指针更新:head->front = (head->front + 1) % MAX; - 打印时指针遍历:
temp = (temp + 1) % MAX;
- 示例:当
rear = MAX-1(数组最后一个下标)时,(rear + 1) % MAX = 0,rear绕回数组起始位置,实现 "循环"。
三、核心功能函数拆解(循环队列的操作实现)
1. 入队操作(InQueue):向队尾添加元素
c
运行
int InQueue(QUEUE *head,int num)
{
// 步骤1:判满,满队列无法入队
if((head->rear+1)%MAX==head->front)
{
printf("Queue is full!\n");
return 0;
}
// 步骤2:将元素存入rear指向的空闲位置
head->elem[head->rear]=num;
// 步骤3:更新rear指针,循环后移一位(指向新的空闲位置)
head->rear=(head->rear+1)%MAX;
// 步骤4:入队成功返回1
return 1;
}
- 操作规则:符合队列 "先进先出" 特性,仅允许在队尾添加元素;
- 关键细节:先存元素,再更新
rear指针,因为rear初始指向空闲位置,存入元素后才需要后移。
2. 出队操作(OUTQueue):从队头删除并返回元素
c
运行
int OUTQueue(QUEUE *head)
{
int iret=0;
// 步骤1:判空,空队列无法出队
if(head->rear==head->front)
{
printf("Queue is empty!\n");
return 0;
}
// 步骤2:取出front指向的有效元素
iret=head->elem[head->front];
// 步骤3:更新front指针,循环后移一位(指向新的队头元素)
head->front=(head->front+1)%MAX;
// 步骤4:返回取出的元素
return iret;
}
- 操作规则:仅允许在队头删除元素,取出的是队列中最先入队的元素;
- 关键细节:先取元素,再更新
front指针,因为front初始指向第一个有效元素; - 注意点:该函数中,空队列返回
0,但0也可能是合法的队列元素,可优化为通过返回值区分 "操作成功 / 失败",元素通过指针参数传出(更严谨)。
3. 打印队列(PrintQueue):遍历并输出所有有效元素
c
运行
void PrintQueue(QUEUE *head)
{
// 步骤1:定义临时指针,保存front初始位置(避免修改原front指针)
int temp=head->front;
// 步骤2:判空,空队列直接提示
if(head->front==head->rear)
{
printf("Queue is empty!\n");
return;
}
// 步骤3:循环遍历,从front到rear(不包含rear,因为rear指向空闲位置)
while(temp!=head->rear)
{
printf("%d ",head->elem[temp]);
// 步骤4:临时指针循环后移
temp=(temp+1)%MAX;
}
}
- 核心技巧:使用临时指针
temp遍历,避免修改队列的front指针(front是队头标识,不可随意改动); - 遍历边界:
temp != head->rear,因为rear指向空闲位置,不是有效元素,遍历到rear前终止。
四、运行结果与解析
1. 预期运行结果(控制台输出)
plaintext
Testing empty queue:
Queue is empty
Enqueuing elements: 10, 20, 30, 40, 50
Current queue: 10 20 30 40 50
Dequeuing elements:
Dequeued: 10
Dequeued: 20
Queue after dequeue: 30 40 50
Enqueuing more elements: 60, 70
Updated queue: 30 40 50 60 70
Dequeuing all elements:
Dequeued: 30
Dequeued: 40
Dequeued: 50
Dequeued: 60
Dequeued: 70
Queue is now empty
2. 关键结果解析
- 入队 5 个元素后,打印结果为
10 20 30 40 50,符合入队顺序; - 出队 2 个元素后,打印结果为
30 40 50,说明出队的是最先入队的10和20,符合 "先进先出"; - 后续入队
60、70后,打印结果为30 40 50 60 70,说明循环队列解决了 "假溢出",充分利用了数组空闲空间; - 清空队列后,再次判空返回
1,说明队列操作逻辑闭环,无残留数据。
五、数据结构专栏扩展知识点(深化理解)
1. 循环队列的两种实现方式对比
| 实现方式 | 优点 | 缺点 |
|---|---|---|
| 顺序存储(数组,本文实现) | 访问效率高(O (1)),实现简单 | 容量固定(静态数组),存在MAX-1的容量浪费 |
| 链式存储(链表) | 容量动态扩展,无容量浪费 | 访问效率略低,需要额外存储指针域,实现稍复杂 |
2. 优化点:解决 "0 作为合法元素的歧义问题"
原OUTQueue函数中,空队列返回0,但0也可能是合法元素,导致调用者无法区分 "队列空" 和 "取出的元素是 0"。优化方案如下:
c
运行
// 优化后:通过返回值区分操作成功/失败,元素通过指针参数传出
int OUTQueue(QUEUE *head, DATATYPE *val)
{
// 步骤1:判空
if(head->front == head->rear)
{
printf("Queue is empty!\n");
return 0; // 0表示出队失败
}
// 步骤2:取出元素,通过指针传出
*val = head->elem[head->front];
// 步骤3:更新front指针
head->front = (head->front + 1) % MAX;
return 1; // 1表示出队成功
}
- 调用示例(修改
main函数中的出队逻辑):
c
运行
DATATYPE val;
if(OUTQueue(queue, &val))
{
printf("Dequeued: %d\n", val);
}
3. 补充:队列的应用场景
循环队列在实际开发中应用广泛,典型场景包括:
- 任务调度(如操作系统的进程调度、打印机任务队列);
- 数据缓冲(如网络通信中的数据接收 / 发送缓冲、日志缓冲);
- 广度优先搜索(BFS)(如二叉树的层次遍历、图的最短路径搜索);
- 异步处理(如消息队列、事件驱动系统)。
六、总结
- 循环队列的核心价值是解决普通顺序队列的 "假溢出" 问题 ,通过
% MAX取模运算实现指针循环移动,充分利用数组空间; - 循环队列的关键是两个核心条件 :
front == rear(判空)、(rear+1)%MAX == front(判满),且实际可用容量为MAX-1; - 队列的核心特性是先进先出(FIFO),所有操作均围绕 "队头出、队尾入" 展开;
- 这份代码是循环队列顺序存储的经典实现,吃透其原理后,可顺利过渡到链式队列、优先级队列等高级队列结构的学习。
这份代码完美契合数据结构专栏的核心知识点,是理解队列结构的绝佳案例,掌握循环队列的实现逻辑,能够为后续学习更复杂的数据结构和算法打下坚实基础