目录
现在,我们聚焦于如何用代码一步步地实现这个最终方案。我们从零开始,只写必要的代码,并在每一步都解释为什么这么写。
数据结构:队列(Queue)与循环队列(Circular Queue)-CSDN博客
第1步:设计蓝图 (The Struct)
从第一性原理出发,要描述一个队列,我们需要知道哪些信息?
-
数据存放在哪里? -> 需要一个指针指向我们的数据区。 (
int* data
) -
队列的总容量是多少? -> 需要一个变量记录容量,这样我们的取模运算才有依据。 (
int capacity
) -
队头在哪里? -> 需要一个下标作为队头指针。 (
int front
) -
队尾在哪里? -> 需要一个下标作为队尾指针。 (
int rear
)
把这些信息组合起来,就形成了我们的“蓝图”——结构体定义。
【代码实现 1:结构体定义】
#include <stdio.h>
#include <stdlib.h> // 我们需要用 malloc 和 free 来动态管理内存
// 循环队列的蓝图
typedef struct {
int* data; // 指向一块用于存储数据的内存
int capacity; // 这块内存的总容量 (实际能存的元素数是 capacity-1)
int front; // 队头元素的下标
int rear; // 队尾元素的下一个位置的下标
} CircularQueue;
这就像建房子前画的设计图,它定义了我们的队列由哪几部分构成。
第2步:队列的诞生 (创建与初始化)
有了蓝图,我们就可以“施工”了,即创建一个具体的队列实例。创建一个容量为 k
的队列,需要做什么?
-
为
CircularQueue
这个结构体本身分配一块内存。 -
为
data
指针指向的数组分配一块内存。根据我们的最终方案,要存储k
个元素,需要k+1
的数组空间。 -
设置初始状态。一个空队列的初始状态是什么?根据我们的约定,是
front
和rear
相等。最简单的就是都设为 0。
【代码实现 2:创建队列函数】
// 功能:创建一个能容纳 k 个元素的空队列
CircularQueue* createQueue(int k) {
// 1. 为队列的整体结构分配内存
CircularQueue* q = (CircularQueue*)malloc(sizeof(CircularQueue));
// 防御性编程:检查内存是否分配成功
if (!q) {
perror("Failed to allocate memory for queue structure");
return NULL;
}
// 2. 设置容量。注意,我们需要 k+1 个空间
q->capacity = k + 1;
// 3. 为实际存储数据的数组分配内存
q->data = (int*)malloc(q->capacity * sizeof(int));
// 再次进行防御性编程
if (!q->data) {
perror("Failed to allocate memory for queue data");
free(q); // 如果数据区分配失败,必须释放之前为结构体分配的内存,防止内存泄漏
return NULL;
}
// 4. 初始化头尾指针,表示队列为空
q->front = 0;
q->rear = 0;
// 5. 返回创建好的队列实例
return q;
}
第3步:状态检查 (判满与判空)
在进行核心操作(入队/出队)之前,必须先能准确判断队列的状态。这就像开车前要先看油表和仪表盘。
-
判空 (isEmpty): 我们的约定是
front == rear
。 -
判满 (isFull): 我们的约定是队尾的下一个位置是队头,即
(rear + 1) % capacity == front
。
这些是实现后续功能的基石。
【代码实现 3:状态判断函数】
// 功能:判断队列是否为空
int isEmpty(CircularQueue* q) {
// 直接翻译我们的约定: front 等于 rear
return q->front == q->rear;
}
// 功能:判断队列是否为满
int isFull(CircularQueue* q) {
// 直接翻译我们的约定: (rear + 1) 对 capacity 取模后等于 front
return (q->rear + 1) % q->capacity == q->front;
}
这两个函数非常简洁,但它们是整个循环队列逻辑的核心。
第4步:核心操作 (入队与出队)
现在,万事俱备,我们可以实现队列的两个核心灵魂操作了。
入队 (Enqueue)
要将一个值 value
加入队尾,需要三步:
-
检查:队列是不是已经满了?如果满了,就不能再入了。这是操作的“前置条件”。
-
放置:如果没满,就把
value
放到rear
指向的位置。 -
更新:
rear
指针需要向前移动一位,为下一次入队做准备。别忘了,要用取模运算来实现循环。
【代码实现 4:入队函数】
// 功能:将一个元素 value 加入队尾
int enqueue(CircularQueue* q, int value) {
// 1. 前置条件检查
if (isFull(q)) {
printf("入队失败:队列已满。\n");
return 0; // 返回 0 表示失败
}
// 2. 放置数据
q->data[q->rear] = value;
// 3. 更新队尾指针,使用取模运算实现循环
q->rear = (q->rear + 1) % q->capacity;
return 1; // 返回 1 表示成功
}
出队 (Dequeue)
要从队头取出一个元素,逻辑类似:
-
检查:队列是不是空的?如果是空的,就无元素可取。这是操作的“前置条件”。
-
取出:如果没空,就从
front
指向的位置获取元素值。 -
更新:
front
指针需要向前移动一位,指向新的队头。同样,要用取模运算。
【代码实现 5:出队函数】
// 功能:从队头取出一个元素,并通过指针 pValue 返回该元素的值
int dequeue(CircularQueue* q, int* pValue) {
// 1. 前置条件检查
if (isEmpty(q)) {
printf("出队失败:队列为空。\n");
return 0; // 失败
}
// 2. 取出数据
*pValue = q->data[q->front];
// 3. 更新队头指针,使用取模运算实现循环
q->front = (q->front + 1) % q->capacity;
return 1; // 成功
}
第5步:善后工作 (销毁队列)
我们用 malloc
申请了内存,在程序结束前,作为一个负责任的程序员,必须将这些内存归还给操作系统,否则会造成内存泄漏。
销毁的顺序应该是:先释放内部的 data
数组,再释放队列结构体本身。
【代码实现 6:销毁函数】
// 功能:释放队列占用的所有内存
void destroyQueue(CircularQueue* q) {
if (q != NULL) {
// 1. 如果 data 指针有效,则先释放它指向的数组内存
if (q->data != NULL) {
free(q->data);
}
// 2. 再释放队列结构体本身的内存
free(q);
}
}
至此,我们已经从一张“蓝图”开始,一步步地构建了创建、检查、操作、销毁一个完整循环队列所需的所有代码。每一步的实现都紧密围绕着我们最初通过第一性原理推导出的核心约定。