目录
引言
队列的概念
队列的实现
单向链表队列
结构
初始化
入队
出队
取队头
取队尾
求个数
判空
内存释放
总结
引言
队列,这个看似普通的数据结构,其实隐藏着无尽的趣味和巧思。就像单向链表这把神奇的魔法钥匙,它能打开队列的奇妙大门。别担心,这不是一场枯燥的科普,而是一场充满冒险和乐趣的队列解密之旅。跟着我,我们一起揭开队列的神秘面纱,探寻它背后的精彩故事吧!
队列的概念
队列是一种特殊的线性表,它限制了数据的插入操作只能在一端进行,而删除操作则只能在另一端进行。这种先进先出(FIFO,First In First Out)的结构赋予了队列独特的特性。在队列中,进行插入操作的一端被称为队尾,而进行删除操作的一端则被称为队头。
队列可以类比为我们在日常生活中经常遇到的排队现象。想象一下你在超市等待结账的队伍,第一个来的人首先被服务,然后是第二个、第三个,以此类推。这就像队列中的先进先出(FIFO)原则,新来的人只能排在队尾,而最先到达的人则首先离开队伍。队列在日常生活中的排队场景中,有效地维持了有序的服务顺序,确保了公平而有序的进行。
队列的实现
队列可以采用数组或链表的结构来实现,其中使用链表结构更为优越。相比数组结构,链表结构的优势在于在队列头部进行出队列操作时,不需要进行元素的搬移(链式结构维护了头指针和尾指针),从而提高了效率。而使用数组结构的话,虽然尾插的效率不错,但是头删的效率就大打折扣了。并且链表结构允许动态地分配和释放内存,更加灵活,适用于处理动态变化的队列大小。因此,使用链表结构实现队列能够更有效地支持队列操作,提升整体性能。
单向链表队列
结构
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
// 定义队列中数据的类型
typedef int QEDataType;
// 定义队列节点的结构体
typedef struct QueueNode
{
QEDataType* val; // 数据指针
struct QueueNode* next; // 下一个节点指针
} QueueNode;
// 定义队列的结构体
typedef struct Queue
{
QueueNode* phead; // 队头指针
QueueNode* ptail; // 队尾指针
int size; // 队列大小
} Queue;
因为这里我们采用的是单向链表来实现队列,所以在执行入队操作时,时间复杂度会达到O(N),因为需要遍历一次链表来找尾,再进行尾插,所以我们干脆用一个结构体来维护这个队列的头节点和尾节点。并且结构体里还有一个size,这个用来指明队列当前数据个数。然后每个节点存储一个数据指针和下一个节点的指针。这种设计方便了队列的插入和删除操作,同时提供了对队列的基本信息的访问。
初始化
void QueueInit(Queue* pq)
{
assert(pq); // 确保队列指针不为空
pq->phead = NULL; // 初始化队头指针为空
pq->ptail = NULL; // 初始化队尾指针为空
pq->size = 0; // 初始化队列大小为0
}
初始化队列的操作就好比给队列找了一个“家”,让队列有了一个干净的、什么都没有的地方,以便之后可以安心地往里面添加元素。这个“家”有两个门,一个是队头,一个是队尾。在开始的时候,队头和队尾都是空的,还没有元素进来。而整个队列的大小也是零,表示里面一个元素都没有。这样,我们就为队列创造了一个清空的、准备好接纳元素的环境。
入队
void QueuePush(Queue* pq, QEDataType val)
{
assert(pq); // 确保队列指针不为空
// 为新元素创建一个队列节点
QueueNode* tmp = (QueueNode*)malloc(sizeof(QueueNode));
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
tmp->val = val; // 将新元素的值存入节点
tmp->next = NULL; // 新节点的下一个节点暂时为空
pq->size++; // 队列大小加一
if (pq->phead == NULL) // 如果队列为空,新元素成为队头和队尾
{
pq->phead = pq->ptail = tmp;
}
else // 如果队列不为空,将新元素追加到队尾,并更新队尾指针
{
pq->ptail->next = tmp;
pq->ptail = tmp;
}
}
想象一下队列就像是排队等候的人们,每个人都是队列中的一个元素。这个函数的作用就好比是有一个新的人想要加入队伍。我们会为这个人创建一个“队列节点”,这个节点就相当于这个新人的位置,用来存储他的信息。然后,我们检查一下队伍有没有空位,如果队伍是空的,这个新人就是队伍的第一位,也是最后一位。如果队伍不是空的,我们就把这个新人加到队尾,然后更新队尾的位置。这样,队伍中就多了一个人,队伍的长度加一。
出队
void QueuePop(Queue* pq)
{
assert(pq); // 确保队列指针不为空
assert(pq->size > 0); // 确保队列不为空
QueueNode* tmp = pq->phead; // 临时指针指向队头
pq->phead = pq->phead->next; // 更新队头指针
free(tmp); // 释放原队头的内存
pq->size--; // 队列大小减一
if (pq->phead == NULL) // 如果队列变为空,更新队尾指针为空
pq->ptail = NULL;
}
首先,我们会找到队头的位置,也就是队伍前面的人,用一个临时指针(tmp)指向这个位置。然后,我们把队头指针往后移动,表示队伍前面的人离开了。接着,我们释放掉原来队头位置的内存,因为这个人已经不在队伍中了。最后,队伍的长度减一,表示队伍中少了一个人。如果队伍变为空,我们还需要把队尾指针更新为空,因为队伍中没有人了,防止出现野指针问题。
取队头
QEDataType QueueFront(Queue* pq)
{
assert(pq); // 确保队列指针不为空
assert(pq->size > 0); // 确保队列不为空
return pq->phead->val; // 返回队头元素的值
}
这里通过我们的头指针很轻松就取到了队头的数据。
取队尾
QEDataType QueueBack(Queue* pq)
{
assert(pq); // 确保队列指针不为空
assert(pq->size > 0); // 确保队列不为空
return pq->ptail->val; // 返回队尾元素的值
}
同理通过尾指针取队尾数据。
求个数
int QueueSize(Queue* pq)
{
assert(pq); // 确保队列指针不为空
return pq->size; // 返回队列的大小
}
直接返回size的值即可。
判空
bool QueueEmpty(Queue* pq)
{
assert(pq); // 确保队列指针不为空
return pq->ptail == NULL; // 如果队尾为空,说明队列为空,返回 true,否则返回 false
}
如果队列为空的话,那么尾指针肯定是空,当然头指针也是为空的。
内存释放
void QueueDestroy(Queue* pq)
{
assert(pq); // 确保队列指针不为空
QueueNode* cur = pq->phead; // 从队头开始遍历队列节点
while (cur)
{
QueueNode* tmp = cur->next; // 保存下一个节点的指针
free(cur); // 释放当前节点的内存
cur = tmp; // 移动到下一个节点
}
pq->phead = pq->ptail = NULL; // 将队头和队尾指针置为空
pq->size = 0; // 队列大小清零
}
从队头开始遍历队列中的每个人(节点),释放每个人所占用的位置(内存)。然后,我们把队头和队尾的位置都设置为空,表示队伍不存在了。最后,队伍中的人数也变成了零,因为队伍已经解散了。这样,我们就成功地销毁了队伍,释放了它所占用的一切。
总结
在这篇博客中,我们一起探索了队列这一数据结构。首先,我们探讨了队列的概念,对它先进先出的特性有了一定程度的了解。接着,我们介绍了队列的实现方式,着重讲解了使用单向链表实现队列的方法。在这一部分,我们探讨了结构设计、初始化、入队、出队、取队头、取队尾、求个数、判空以及内存释放等关键操作,使读者对队列的操作有了基础的了解。希望通过这篇博客,能让读者对队列有更全面的认识,并能够在未来的编程之旅中更加游刃有余地运用队列这一强大的工具。如果你队列的实现对你来说已经不在话下了,可以前往力扣选择队列的题目开冲。