当前位置: 首页 > news >正文

链式队列:高效实现O(1)入队出队

引言

在之前的文章中,我们系统学习了栈结构(顺序栈和链栈)。栈是"后进先出"(LIFO)的结构,而今天要讲解的队列(Queue)则是"先进先出"(FIFO,First In First Out)的结构。

队列在计算机科学中应用极为广泛:操作系统的任务调度、消息队列、打印队列、网络数据包缓冲……几乎所有需要"排队处理"的场景都离不开队列。

本文将详细讲解链式队列的完整实现。链式队列使用链表节点动态存储元素,配合头尾指针实现 O(1) 的入队和出队操作。

第一部分:链式队列的设计原理

一、核心数据结构

链式队列由两个结构体组成:

// 1. 节点结构体 typedef struct Queuenode { ElempType val; // 数据域 struct Queuenode* next; // 指针域,指向下一个节点 } Queuenode, *PQueuenode; // 2. 队列管理结构体(带头结点) typedef struct LinkQueue { PQueuenode front; // 队头指针(指向头结点) PQueuenode rear; // 队尾指针(指向最后一个节点) } LinkQueue, *PLinkQueue;

二、关键设计:头结点 + 双指针

三、队列状态判断

状态frontrear条件
空队列指向头结点指向头结点front == rear
有元素指向头结点指向最后一个节点front != rear
一个元素指向头结点指向唯一节点front->next == rear
已销毁NULLNULLfront == NULL && rear == NULL

四、为什么需要头结点和尾指针

设计要素作用
头结点统一空队列和非空队列的插入/删除操作,避免判空特殊处理
front 指针始终指向头结点,方便出队操作(O(1))
rear 指针始终指向最后一个节点,方便入队操作(O(1))

第二部分:初始化与销毁

一、创建节点

PQueuenode buynode(ElempType val) { PQueuenode p = (PQueuenode)malloc(sizeof(Queuenode)); if (p == NULL) return NULL; p->next = NULL; p->val = val; return p; }

二、初始化(带头结点)

void InitLinkQueue(PLinkQueue ps) { assert(ps != NULL); // 创建头结点 PQueuenode p = buynode(0); if (p == NULL) return; // 空队列:front 和 rear 都指向头结点 ps->rear = p; ps->front = p; }

初始化后的状态

三、销毁

void Destroy(PLinkQueue ps) { assert(ps != NULL); if (Isempty(ps)) return; // 逐个释放所有节点(包括头结点) while (ps->front != NULL) { PQueuenode p = ps->front; ps->front = p->next; free(p); } // 指针置空 ps->rear = NULL; }

清空 vs 销毁

// 清空:保留头结点,只释放数据节点 void Clear(PLinkQueue ps) { assert(ps != NULL); if (Isempty(ps)) return; while (ps->front->next != NULL) { PQueuenode p = ps->front->next; ps->front->next = p->next; free(p); } ps->rear = ps->front; // rear 回到头结点 } // 销毁:释放所有节点,包括头结点 void Destroy(PLinkQueue ps) { // 释放所有节点,指针置 NULL }
操作头结点frontrear后续使用
清空保留指向头结点指向头结点可继续使用
销毁释放NULLNULL需重新初始化

第三部分:核心操作实现

一、入队(Push)

bool Push(PLinkQueue ps, ElempType val) { assert(ps != NULL); // 1. 创建新节点 PQueuenode p = buynode(val); if (p == NULL) return false; // 2. 插入到队尾 ps->rear->next = p; // 当前最后一个节点的 next 指向新节点 ps->rear = p; // rear 后移 return true; }

入队过程图解

入队的特殊情况

// 空队列入队:
// 入队前:front == rear == 头结点
// 入队后:front == 头结点, rear == 新节点
// 代码逻辑完全适用,无需特殊处理!

二、出队(Pop)

bool Pop(PLinkQueue ps, ElempType* pval) { assert(ps != NULL && pval != NULL); if (Isempty(ps)) return false; // 1. 获取队头节点(头结点后的第一个节点) PQueuenode p = ps->front->next; // 2. 获取数据 *pval = p->val; // 3. 跳过被删除的节点 ps->front->next = p->next; // 4. 特殊处理:如果删除的是最后一个节点,rear 要回退 if (p == ps->rear) { ps->rear = ps->front; } // 5. 释放节点 free(p); return true; }

注意:原代码中缺少了rear回退的特殊处理。当队列只有一个元素时,出队后rear应该指向头结点。

修正后的代码

bool Pop(PLinkQueue ps, ElempType* pval) { assert(ps != NULL && pval != NULL); if (Isempty(ps)) return false; PQueuenode p = ps->front->next; *pval = p->val; ps->front->next = p->next; // ★ 关键:删除的是最后一个节点时,更新 rear if (p == ps->rear) { ps->rear = ps->front; } free(p); return true; }

三、获取队头和队尾

// 获取队头元素 bool Getfront(PLinkQueue ps, ElempType* pval) { assert(ps != NULL); if (Isempty(ps)) return false; *pval = ps->front->next->val; // 头结点后的第一个节点 return true; } // 获取队尾元素 bool Getrear(PLinkQueue ps, ElempType* pval) { assert(ps != NULL); if (Isempty(ps)) return false; *pval = ps->rear->val; // 直接通过 rear 指针获取 return true; }

四、判空

bool Isempty(const PLinkQueue ps) { assert(ps != NULL); return ps->front == ps->rear; }

五、获取元素个数

int Getsize(const PLinkQueue ps) { assert(ps != NULL); int n = 0; for (Queuenode* p = ps->front->next; p != NULL; p = p->next) { n++; } return n; }

注意:这里的获取大小是 O(n) 遍历。如果想 O(1),可以在LinkQueue结构体中添加cursize成员。


第四部分:完整操作一览

时间复杂度分析

操作时间复杂度说明
初始化O(1)创建头结点
判空O(1)比较 front 和 rear
入队 (Push)O(1)直接在 rear 后插入
出队 (Pop)O(1)删除 front->next
获取队头O(1)读取 front->next->val
获取队尾O(1)读取 rear->val
获取大小O(n)需要遍历(可优化为 O(1))
清空/销毁O(n)逐个释放节点

操作要点速查

第六部分:链式队列 vs 顺序队列

对比项链式队列顺序队列(循环队列)
存储方式离散节点连续数组
容量限制无(内存足够即可)有(预分配或扩容)
入队/出队O(1)O(1)
内存开销每节点多一个指针仅数据空间
缓存友好差(内存不连续)好(内存连续)
实现复杂度稍复杂需要处理循环下标
适用场景元素数量不可预估元素数量可预估

总结

一、链式队列的核心原理

二、操作复杂度速查

操作时间复杂度关键步骤
入队O(1)rear->next = p; rear = p;
出队O(1)p = front->next; front->next = p->next; free(p);
获取队头O(1)front->next->val
获取队尾O(1)rear->val
判空O(1)front == rear

三、一句话记忆

链式队列用 front 和 rear 两个指针分别指向头结点和尾节点,入队时在 rear 后面插入并更新 rear,出队时删除 front 后面的节点,实现 O(1) 的入队和出队操作。出队时如果删除了最后一个节点,必须将 rear 回退到头结点。

http://www.jsqmd.com/news/815646/

相关文章:

  • 分期乐额度变现避坑指南,新手也能安全操作 - 米米收
  • 双屏异显POS主板方案:RK3288芯片如何重塑智慧零售收银体验
  • 3步快速清理重复图片:AntiDupl.NET智能去重完整指南
  • 破解电气安全管控痛点:电气检测公司如何通过3C闭环方法论实现全场景安全合规? - 速递信息
  • 2026最新新疆婚纱摄影工作室品牌排行:5家机构实地评测对比 - 奔跑123
  • 如何利用QuPath批量处理65张病理图像的多通道复制难题?
  • 如何用Midjourney 1小时内产出可商用酒标?——含版权合规检测清单、CMYK预校准技巧与Pantone色号映射表
  • 物联网B2B网站哪个实力强?智能制造网深度测评 - 品牌推荐大师1
  • 2026年微⽔泥砖厂家权威推荐选择:芒果瓷砖 - 品牌推广大师
  • 【Python | matplotlib】从入门到精通:matplotlib.cm颜色映射的实战应用与自定义指南
  • Midscene.js:重新定义AI驱动的跨平台视觉自动化架构
  • HoRain云--MySQL排序技巧与PHP实战指南
  • 别再满世界找grep了!Windows上PowerShell自带的Select-String和findstr,5分钟上手教程
  • 【渗透测试】国家信息安全漏洞共享平台
  • ElevenLabs罗马尼亚语音项目交付倒计时:3天内必须完成的4项本地化校验(含重音符号映射表+词形变化兼容清单)
  • Geckodriver终极指南:快速安装Firefox自动化测试工具
  • 速看!2026年国内无线电磁流量计品牌TOP10揭秘 - 仪表人叶工
  • 无锡全网热议的纹眉怎么选不踩坑?久匠十年连锁,做眉自然又高级 - 企业博客发布
  • 选电磁流量计看什么?十大品牌核心参数横评 - 仪表人叶工
  • 《另一个伊甸》全副本职业书掉落指南与角色养成对照
  • Pearcleaner:开源透明的Mac应用清理工具,彻底释放存储空间
  • AnuPpuccin主题:面向Obsidian用户的可定制化视觉框架
  • 深度解析 CMVR认证:一篇读懂印度汽车市场准入核心要求 - 速递信息
  • 基于MCP协议的本地化地址数据处理工具:sthan-mcp-server深度解析
  • 【仅开放至2026年6月30日】头部AI实验室内部TTS性能基准测试报告(含VALL-E X、Fish-Speech 2.1、Azure Neural TTS v5等11引擎盲测排名)
  • 第十一节:多检索查询、混合检索(多检索+RRF重排)、检索后优化(文档压缩)
  • 对比官方价格Taotoken活动价在长期使用中的成本优势感知
  • ISO 9001认证如何保障嵌入式测试工具TESSY的质量与可靠性
  • 嘉兴精致女生都在做的纹眉,久匠真的值得入手吗?审美在线气质翻倍 - 企业博客发布
  • Kibana 7.3.0 导出CSV报告保姆级教程:从保存搜索到解决内存溢出