前言:
日常生活中,"排队"是再熟悉不过的场景——无论是食堂打饭、超市结账、医院挂号,还是小程序等餐,都遵循着"先到先得"的基本原则。在计算机科学中,队列(Queue)是对"排队逻辑"的抽象实现,作为遵循「先进先出(First In First Out, FIFO)」原则的线性数据结构,它已成为算法和工程领域最常用的基础数据结构之一。
本文系统讲解队列这一数据结构,从基础概念到实际应用进行全面剖析。通过清晰的逻辑和丰富的实例,即使是数据结构初学者也能轻松掌握核心要点。

一、队列的核心概念
1.1队列的定义
队列是一种特殊的线性表,其只允许在一端进行插入数据操作,在另一端进行删除数据操作,队列具有先进先出 FIFO(First In First Out)。
1.2队列的基础知识
为了方便理解,先要明确队列的基础知识
front 指针:始终指向 “最早入队的元素”(队头),出队时仅移动 front,保证先入队的元素先被移除 。
tail指针:始终指向“下一个入队元素的位置(队尾的后一位)”,入队时仅移动tail,保证新元素只能从队尾加入。
出队:始终删除链表头部(front 指向的节点),而头部节点正是 “最早入队的元素”。
入队和出队相结合,完美契合了FIFO,先入的元素先出,后入的元素后出。
1.3进队出队的示意图
入队(EnQueue):新元素始终接在链表尾部(tail指向的节点),保证进入顺序与链表节点顺序一致。
出队(DeQueue):始终删除链表头部(front 指向的节点),而头部节点正是 “最早入队的元素”。
入队和出队相结合,完美契合了FIFO,先入的元素先出,后入的元素后出。

二、队列的实现
本文通过使用链表为底层数据结构来进行实现队列
如下图所示:通过对单向链表进行头删和尾删的方式进行模拟队列出队和入队。

2.1队列实现的相关接口

2.2队列实现的相关文件

2.3Queue.h文件
Queue.h头文件实现:通过结构体定义队列,声明队列的相关接口函数。
#pragma once
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#include<stdio.h>
typedef int QDataType;
//创建链式节点
typedef struct QListNode
{
struct QListNode* next;
QDataType val;
}QNode;
//定义一个结构体,成员为头节点指针和尾节点指针
typedef struct Queue
{
QNode* phead;
QNode* ptail;
//用于记录队列的元素个数
int size;
}Queue;
//队列初始化
void QueueInit(Queue * q);
//队尾插入
void QueuePush(Queue* q, QDataType x);
//队头删除
void QueuePop(Queue* q);
//获取队头元素
QDataType QueueFront(Queue *q);
//获取队尾元素
QDataType QueueBack(Queue *q);
//获取队列有效元素个数
int QueueSize(Queue *q);
//判断队列是否为空
bool QueueEmpty(Queue *q);
//销毁队列
void QueueDestroy(Queue *q);
对于Queue.h文件主要关注如下代码:
typedef int QDataType;
//创建链式节点
typedef struct QListNode
{
struct QListNode* next;
QDataType val;
}QNode;
//定义一个结构体,成员为头节点指针和尾节点指针
typedef struct Queue
{
//队列头节点指针
QNode* phead;
//队列尾节点指针
QNode* ptail;
//用于记录队列的元素个数
int size;
}Queue;
1.struct QListNode 创建链表节点的结构体
成员一:struct QListNode* next; 用于指向下一个节点的指针
成员二:储存在节点中的值,通过typedef int QDataType ,方便后续数据类型的替换
typedef struct QListNode QNode 重命名结构体
2. struct Queue 维护队列指针的结构体
成员一:队列头节点指针 QNode* phead;
成员二:队列尾节点指针 QNode* ptail;
成员三:队列中的元素个数 size
思考1:为什么要定义一个结构体专门用于维护队列指针?
如果队列初始时为空,以及队尾插入元素需要改变头节点指针和尾节点指针,需要使用二级指针过于麻烦
对于函数接口的调用过于复杂,不利于函数的使用。
如下函数为例:
void QueuePush(QNode **phead , QNode **pptail ,QDataType x);
而通过队列指针结构体维护就可以更加方便:
void QueuePush(Queue* q, QDataType x);
思考2:为什么这里只需要传入Queue* ,这个结构体的一级指针,而上述需要传入QNode **,这个结构体的二级指针呢?
对于需要传入QNode **这个指针:
举个典型场景:向空队列插入第一个节点
初始状态:QNode *phead = NULL,*ptail = NULL(队列为空);
插入节点后:需要让phead 和 ptail 都指向新节点(phead = newNode,ptail = newNode)。
此时,函数内部需要修改外部的phead和ptail(指针本身的指向)。
由于函数参数是值传递:
若传入QNode* phead(一级指针),函数内修改的是phead的副本,外部的原phead仍为NULL(无效);
必须传入QNode**phead(二级指针,即 “指针的指针”),才能通过*phead = newNode修改外部phead的指向。
简言之:当需要修改 “指针变量本身” 时,必须用二级指针。
对于只需要传入Queue* :
因为入队操作只需要修改front的指向 和 tail的指向 ,而不是修改这个Queue* q这个一级指针指向,才需要使用Queue** pq。
还是以 “向空队列插入第一个节点” 为例:
初始状态:q->front = NULL,q->tail = NULL(结构体成员为空);
插入节点后:需要修改q->front = newNode,q->tail = newNode(修改结构体内部的指针成员)。
由于函数参数是Queue* q(一级指针):
通过q可以直接访问结构体成员(q->front),并修改成员的指向(q->front = newNode);这里不需要修改q本身的指向(q始终指向同一个队列结构体),因此一级指针足够。
简言之:当只需要修改 “结构体内部的成员”(即使成员是指针),一级结构体指针足够。
思考3:为什么单链表的实现不使用头节点指针和尾节点指针方式实现?
因为尾删时需要找到尾节点前一个节点,定义尾节点意义不大,而对于队列而言只能从队列的头部进行删除,从队列的尾部进行插入。
2.4Queue.c文件
搞定了上面三个思考问题,那么我们就可以轻松+愉快的实现队列了!
①队列初始化
void QueueInit(Queue* q)
{
assert(q);
//队列的头指针和尾指针都为空
q->phead = q->ptail = NULL;
q->size = 0;
}
②入队
//入队
void QueuePush(Queue* q, QDataType x)
{
assert(q);
QNode* newnode = (QNode *)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail !");
return;
}
newnode->val = x;
newnode->next = NULL;
//队头指针为空
if (q->phead == NULL)
{
q->phead = q->ptail = newnode;
}
else
{
//队尾插入节点
q->ptail->next = newnode;
//更新队尾指针位置
q->ptail = q->ptail->next;
}
//更新队列元素个数
q->size++;
}
温馨提示:
当队列为空时,需要进行入队,则需要同时改变队头指针和队尾指针,使其指向新节点。
③出队
//出队
void QueuePop(Queue* q)
{
assert(q);
//判断是否能够进行删除
assert(q->size > 0);
//只有一个节点时
if (q->phead->next == NULL)
{
free(q->phead);
q->phead = q->ptail = NULL;
}
else
{
//保存头节点下一个节点
QNode* tmp = q->phead->next;
//释放当前头节点
free(q->phead);
//更新头节点位置
q->phead =tmp;
}
//更新队列元素个数
q->size--;
}
温馨提示:
当队列只有一个节点时,且需要进行出队,需要进行同时改变队头指针和队尾指针,否则在该节点释放后,队尾指针成为了野指针。
如下图所示:
④获取队头元素
//获取队头元素
QDataType QueueFront(Queue* q)
{
assert(q);
assert(q->size > 0);
return q->phead->val;
}
⑤获取队尾元素
//获取队尾元素
QDataType QueueBack(Queue* q)
{
assert(q);
assert(q->size > 0);
return q->ptail->val;
}
⑥获取队列的有效元素个数
//获取队列的有效元素个数
int QueueSize(Queue* q)
{
assert(q);
return q->size;
}
⑦判断队列是否为空
//判断队列是否为空
bool QueueEmpty(Queue* q)
{
assert(q);
return q->size == 0;
}
⑧销毁队列
//销毁队列
void QueueDestroy(Queue* q)
{
assert(q);
QNode* pcur = q->phead;
while (pcur)
{
QNode* tmp = pcur->next;
free(pcur);
pcur = tmp;
}
q->phead = q->ptail = NULL;
q->size = 0;
}
2.5Test.c文件
#include"Queue.h"
// 测试基本入队、出队及元素获取
void TestQueueBasic()
{
Queue q;
QueueInit(&q);
// 测试空队列初始状态
printf("=== 测试空队列初始状态 ===\n");
printf("队列是否为空:%s\n", QueueEmpty(&q) ? "是" : "否");
printf("初始元素个数:%d\n", QueueSize(&q));
printf("\n");
// 测试入队操作
printf("=== 测试入队操作 ===\n");
QueuePush(&q, 10);
QueuePush(&q, 20);
QueuePush(&q, 30);
QueuePush(&q, 40);
printf("入队4个元素后,元素个数:%d\n", QueueSize(&q));
printf("当前队头元素:%d(预期:10)\n", QueueFront(&q));
printf("当前队尾元素:%d(预期:40)\n", QueueBack(&q));
printf("\n");
// 测试单次出队
printf("=== 测试单次出队 ===\n");
QueuePop(&q);
printf("出队1个元素后,元素个数:%d\n", QueueSize(&q));
printf("当前队头元素:%d(预期:20)\n", QueueFront(&q));
printf("当前队尾元素:%d(预期:40)\n", QueueBack(&q));
printf("\n");
// 测试多次出队至仅剩一个元素
printf("=== 测试多次出队至仅剩一个 ===\n");
QueuePop(&q);
QueuePop(&q);
printf("再出队2个元素后,元素个数:%d\n", QueueSize(&q));
printf("当前队头元素:%d(预期:40)\n", QueueFront(&q));
printf("当前队尾元素:%d(预期:40)\n", QueueBack(&q));
printf("\n");
// 测试出队至空队列
printf("=== 测试出队至空队列 ===\n");
QueuePop(&q);
printf("全部出队后,元素个数:%d\n", QueueSize(&q));
printf("队列是否为空:%s\n", QueueEmpty(&q) ? "是" : "否");
printf("\n");
// 测试空队列入队新元素
printf("=== 测试空队列重新入队 ===\n");
QueuePush(&q, 50);
QueuePush(&q, 60);
printf("重新入队2个元素后,元素个数:%d\n", QueueSize(&q));
printf("当前队头元素:%d(预期:50)\n", QueueFront(&q));
printf("当前队尾元素:%d(预期:60)\n", QueueBack(&q));
QueueDestroy(&q);
}
// 测试混合入队出队场景
void TestQueueMixed()
{
Queue q;
QueueInit(&q);
printf("\n\n=== 测试混合入队出队场景 ===\n");
// 交替入队出队
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePop(&q);
QueuePush(&q, 3);
QueuePop(&q);
QueuePush(&q, 4);
QueuePush(&q, 5);
QueuePop(&q);
printf("混合操作后,元素个数:%d(预期:2)\n", QueueSize(&q));
printf("当前队头元素:%d(预期:4)\n", QueueFront(&q));
printf("当前队尾元素:%d(预期:5)\n", QueueBack(&q));
QueueDestroy(&q);
}
int main()
{
TestQueueBasic(); // 基础功能测试
TestQueueMixed(); // 混合操作测试
return 0;
}

三、队列与栈的简单对比
3.1核心定义对比
| 对比维度 | 栈(Stack) | 队列(Queue) |
|---|---|---|
| 核心定义 | 仅允许在 “一端”(栈顶,Top)进行插入和删除的数据结构 | 仅允许在 “一端”(队尾,Rear)插入、“另一端”(队头,Front)删除的数据结构 |
| 核心原则 | LIFO(Last In First Out,后进先出) | FIFO(First In First Out,先进先出) |
| 形象类比 | 叠放的盘子:只能从最顶端放新盘子,也只能从顶端拿盘子 | 排队买票:先到的人先买票,后到的人只能排到队尾 |
3.2关键操作与特性对比
| 对比维度 | 栈(Stack) | 队列(Queue) |
|---|---|---|
| 核心操作名称 |
- 插入: - 删除: - 查看: |
- 插入: - 删除: - 查看: |
| 数据存取限制 | 仅允许在栈顶进行插入 / 删除,不支持随机访问(无法直接访问栈中间元素) | 仅允许在队尾插入、队头删除,不支持随机访问(无法直接访问队列中间元素) |
| 常用操作时间复杂度 |
- - peek查找指定元素:O (n)(需从栈顶依次遍历) |
- - peek查找指定元素:O (n)(需从队头依次遍历) |
| 物理实现方式 |
可基于数组或链表实现: - 数组栈:需注意 “栈满” 扩容 - 链表栈:无固定大小,灵活扩容 |
可基于数组或链表实现: - 数组队列:易出现 “假溢出”(需用循环队列优化) - 链表队列:无固定大小,灵活扩容(通常用 “带头尾指针的链表” 实现) |
既然看到这里了,不妨点赞+收藏,感谢大家,若有问题请指正。




988

被折叠的 条评论
为什么被折叠?



