
🏠个人主页:黎雁
🎬作者简介:C/C++/JAVA后端开发学习者
❄️个人专栏:C语言、数据结构(C语言)、EasyX、游戏、规划、程序人生
✨ 从来绝巘须孤往,万里同尘即玉京

文章目录
- 栈与队列之队列入门攻略:从核心概念到链表实现✨
-
- 文章摘要
- [一、知识回顾:栈 vs 队列的核心差异](#一、知识回顾:栈 vs 队列的核心差异)
- 二、队列的核心概念:什么是"先进先出"?📚
-
- [1. 队列的定义](#1. 队列的定义)
- [2. 队列的关键操作术语](#2. 队列的关键操作术语)
- [3. 队列的实现方式对比:数组 vs 链表](#3. 队列的实现方式对比:数组 vs 链表)
- 三、队列的工程化实现:链表版队列完整代码💻
-
- [1. 头文件定义(queue.h):接口声明](#1. 头文件定义(queue.h):接口声明)
- [2. 源文件实现(queue.c):核心逻辑](#2. 源文件实现(queue.c):核心逻辑)
- 四、核心易错点解析:边界场景处理⚠️
- 五、写在最后
栈与队列之队列入门攻略:从核心概念到链表实现✨
你好!欢迎来到线性表系列栈与队列篇的第二篇内容~
在上一篇中,我们吃透了"后进先出"的栈结构,而今天的主角------队列,恰恰是栈的"反向搭档",它遵循"先进先出"原则,在日常开发和算法解题中同样不可或缺,比如消息队列、任务排队、二叉树层序遍历等场景都能看到它的身影。
继栈的数组实现后,我们这篇将聚焦队列的核心知识,从概念到实现,手把手带你搞定这个"排队专用"的线性表!
文章摘要
本文为线性表系列栈与队列篇第二篇入门内容,聚焦队列这一受限线性表。通俗讲解队列"先进先出"的核心特性,对比数组与链表两种实现方式的优劣并选定单向链表为最优方案。通过工程化的头文件+源文件结构,完整实现队列的初始化、销毁、入队、出队等核心操作,详解队头队尾指针的边界处理细节,补充空队列、单节点队列等特殊场景的处理逻辑,零基础吃透队列的底层实现。
阅读时长 :约20分钟
阅读建议:
- 基础薄弱者:先理解队列"先进先出"特性,再对照代码梳理指针逻辑
- 工程开发者:重点关注内存释放、队空/队满判断等健壮性细节
- 面试备考者:牢记队列的特性、实现方式及典型应用场景
- 查漏补缺者:直接查看出队操作中单节点队列的特殊处理逻辑
一、知识回顾:栈 vs 队列的核心差异
在学习队列之前,我们先对比栈和队列的核心区别,快速建立认知:
| 数据结构 | 核心特性 | 操作端 | 最优实现方式 |
|---|---|---|---|
| 栈 | 后进先出(LIFO) | 仅栈顶一端 | 数组(尾插尾删) |
| 队列 | 先进先出(FIFO) | 队尾入、队头出 | 链表(头删尾插) |
简单总结:栈是"一口进出",队列是"一头进、另一头出"!
二、队列的核心概念:什么是"先进先出"?📚
1. 队列的定义
队列是一种特殊的线性表,只允许在一端插入元素 (队尾),在另一端删除元素 (队头),遵循先进先出(FIFO = First In First Out) 原则。
生活中的队列例子随处可见:
- 排队买奶茶:先排队的人先买到奶茶 → 完美契合"先进先出"
- 打印机打印任务:先提交的任务先打印 → 典型的队列应用
- 消息队列:先产生的消息先被消费 → 工程中最常用的队列场景
2. 队列的关键操作术语
| 操作名称 | 说明 | 操作位置 |
|---|---|---|
| 入队/进队 | 向队列中添加元素 | 队尾(唯一入队端) |
| 出队/离队 | 从队列中删除元素 | 队头(唯一出队端) |
| 队头 | 允许删除元素的一端 | - |
| 队尾 | 允许插入元素的一端 | - |
3. 队列的实现方式对比:数组 vs 链表
队列同样有两种实现方式,我们详细分析优劣,选择最优方案:
| 实现方式 | 核心思路 | 优点 | 缺点 |
|---|---|---|---|
| 数组 | 队尾入队(尾插)、队头出队(头删) | 实现简单、随机访问快 | 队头出队需挪动所有元素,时间复杂度O(N),效率极低 |
| 链表(推荐) | 单向链表:队尾入队(尾插)、队头出队(头删) (用tail指针记录队尾,避免找尾) | ① 入队/出队效率O(1) ② 按需分配内存,无需扩容 | 需额外维护队头/队尾指针,处理空队列/单节点队列边界 |
✅ 结论:单向链表实现队列是最优选择!
- 链表头删、尾插的时间复杂度都是O(1),完美匹配队列的操作需求
- 只需维护head(队头)和tail(队尾)两个指针,即可高效完成所有操作
- 避免了数组出队时挪动元素的性能损耗
三、队列的工程化实现:链表版队列完整代码💻
我们依旧采用"头文件+源文件"的工程化结构实现队列,保证代码规范性。
1. 头文件定义(queue.h):接口声明
c
#pragma once
#include <stdio.h>
#include <stdbool.h>
#include <assert.h>
#include <stdlib.h>
// 定义队列存储的数据类型
typedef int QDataType;
// 队列节点结构体(单向链表节点)
typedef struct QueueNode
{
struct QueueNode* next; // 指向下一个节点的指针
QDataType data; // 节点存储的数据
}QNode;
// 队列结构体(维护队头、队尾指针)
typedef struct Queue
{
QNode* head; // 队头指针(出队端)
QNode* tail; // 队尾指针(入队端)
}Queue;
// 队列核心操作接口
void QueueInit(Queue* pq); // 初始化队列
void QueueDestroy(Queue* pq); // 销毁队列(释放内存)
void QueuePush(Queue* pq, QDataType x); // 入队(队尾插入)
void QueuePop(Queue* pq); // 出队(队头删除)
QDataType QueueFront(Queue* pq); // 获取队头元素
QDataType QueueBack(Queue* pq); // 获取队尾元素
int QueueSize(Queue* pq); // 获取队列元素个数
bool QueueEmpty(Queue* pq); // 判断队列是否为空
2. 源文件实现(queue.c):核心逻辑
c
#include "queue.h"
// 初始化队列
void QueueInit(Queue* pq)
{
assert(pq); // 确保队列指针不为NULL
pq->head = NULL; // 初始队头为空
pq->tail = NULL; // 初始队尾为空
}
// 销毁队列(遍历释放所有节点)
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur) // 逐个释放节点
{
QNode* next = cur->next; // 保存下一个节点
free(cur);
cur = next;
}
// 重置指针,防止野指针
pq->head = NULL;
pq->tail = NULL;
}
// 入队(队尾插入)
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
// 创建新节点
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
// 处理空队列(第一个节点)
if (pq->tail == NULL)
{
pq->head = newnode;
pq->tail = newnode;
}
else // 队列非空,尾插
{
pq->tail->next = newnode;
pq->tail = newnode; // 更新队尾指针
}
}
// 出队(队头删除)
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq)); // 队空禁止出队
// 处理单节点队列(删除后队头队尾都为空)
if (pq->head->next == NULL)
{
free(pq->head);
pq->head = NULL;
pq->tail = NULL;
}
else // 多节点队列,头删
{
QNode* next = pq->head->next;
free(pq->head);
pq->head = next; // 更新队头指针
}
}
// 获取队头元素
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq)); // 队空禁止获取
return pq->head->data;
}
// 获取队尾元素
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq)); // 队空禁止获取
return pq->tail->data;
}
// 获取队列元素个数
int QueueSize(Queue* pq)
{
assert(pq);
int size = 0;
QNode* cur = pq->head;
while (cur) // 遍历统计节点数
{
size++;
cur = cur->next;
}
return size;
}
// 判断队列是否为空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL; // 队头为空则队列空
}
四、核心易错点解析:边界场景处理⚠️
队列实现中最容易出错的是边界场景,我们重点梳理:
- 空队列入队:需同时更新head和tail指针,否则会出现tail为NULL的错误
- 单节点队列出队:删除后必须将head和tail都置NULL,否则tail会指向已释放的节点(野指针)
- 出队前判空 :必须用
assert(!QueueEmpty(pq))防止空队列出队,避免访问NULL指针
五、写在最后
恭喜你!这一篇中你已经掌握了队列的核心知识:
- 理解了队列"先进先出"的核心特性,对比了和栈的关键差异
- 选定了链表作为队列的最优实现方式,吃透了头删尾插的逻辑
- 完成了队列的工程化代码实现,掌握了边界场景的处理技巧
队列和栈是数据结构的"基础搭档",二者结合能解决很多复杂问题:
- 算法层面:二叉树层序遍历、BFS(广度优先搜索)依赖队列
- 工程层面:消息队列、任务调度、限流削峰都离不开队列
下一篇,我们将进入实战环节,用栈和队列解决经典OJ题,把理论转化为解题能力!😜
点赞+收藏+关注,跟着系列内容一步步吃透栈和队列!你的支持是我创作的最大动力~👍