恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

队列数据结构详解:从FIFO到循环队列、消息队列与阻塞队列

  • 首页
  • 资讯中心
  • /
  • 队列数据结构详解:从FIFO到循环队列、消息队列与阻塞队列

相关资讯

C语言课后习题答案PDF怎么用?四步对答案法避坑指南 2026/10/17 7:19:39
C++ UDP Socket通信实战:从协议选型到完整实现与排错 2026/10/17 7:19:39
一文读懂半导体:从材料到芯片的完整原理与制造流程 2026/10/17 7:14:39

最新资讯

大模型调用链的数据安全从模型路由到企业级反萃取:TaoToken 统一 Key 通道下的可复制配置与验证
打工人刚需!OpenClaw 中文汉化部署教程:从零到可用的一站式配置
第5讲:MCP 协议实战——把数据库、文件系统、API、浏览器变成 Agent 工具,TaoToken 统一 Key 接入
C++迷宫游戏:面向对象工程实践与跨平台终端开发
OpenClaw 安全使用全攻略:从 Docker 本地部署到权限加固,零风险 AI 智能体实战
AI广告新玩法:把技术缺陷变成笑点与传播资产

今日推荐

Qwen3.5-122B-A10B-singprobe幻觉检测实战:AUC 0.7806,如何实时揪出大模型一本正经的胡话?
NetSpot 实战:用信道分析与频宽优化解决 Wi-Fi 卡顿
C++ split() 字符串分割的五种实现与性能实测

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

队列数据结构详解:从FIFO到循环队列、消息队列与阻塞队列

发布时间:2026/10/17 7:19:39
队列数据结构详解:从FIFO到循环队列、消息队列与阻塞队列 1. 食堂排队的规则就是队列最本质的逻辑先说个生活中的场景。中午在食堂打饭窗口前排了一队人大家都自觉站在队尾窗口每叫一个人队伍最前面的那个人往前走一步。后来的同学不会跑到最前面去挤否则旁边的人都会提醒他“排队排队”。这就是队列最朴素的规则先来的人排在队首、先被服务后来的排在队尾、最后被服务。程序里的队列Queue就是把这条规则原封不动搬进内存。它只允许在队尾插入数据入队enqueue在队首删除数据出队dequeue中间的数据不许直接拿出来看更不允许从中间插一个进来。我第一次认真理解队列不是背概念而是想明白了一个问题为什么要规定得这么死数组明明可以随便访问任意位置链表也可以随便插入为什么非要把自己限制成只能头出尾进后来才发现这个“限制”恰恰是队列存在的意义。它把“先来先服务”这个需求固化成了数据结构本身任何人用队列都不用担心自己的代码会把一个后到的数据先处理掉。规则写在结构里比靠程序员的自觉要靠谱得多。1.1 先进先出先到的先被服务先进先出英文是First In First Out简称FIFO。与之相对的是栈的LIFOLast In First Out后进先出。这两个名词是面试中经常出现的概念但要理解它们不靠背定义而是靠能举出对得上号的例子FIFO对应排队买票、取号、请求处理先到先得。LIFO对应叠盘子最后放上去的盘子最先被拿走或者浏览器后退按钮最后打开的页面最先被回退。有位前辈跟我说过一句话我一直记到现在栈和队列的差别不在存储方式而在谁先被处理。数组和链表都能实现这两种结构存储上没什么本质变化变的是访问顺序。这句话让我少走了很多弯路因为它把讨论重心从“用什么存”引到了“怎么取”上。顺着这个思路往后走你会自然注意到队列的数据流看起来就像一个管道一头进另一头出顺序始终不变。这个特性决定了它能应用的场景也会在后续章节里反复出现。1.2 队列和栈一个先进先出一个后进先出如果将队列和栈放在一张表里对比差异会非常清楚对比项队列Queue栈Stack数据进出顺序先进先出FIFO后进先出LIFO允许操作的位置队尾插入、队首删除栈顶插入、栈顶删除生活类比排队打饭、取号叫号叠盘子、浏览器后退按钮典型应用消息队列、BFS、任务调度函数调用栈、表达式求值、DFS这个对比不只用于面试更能在实际设计里帮我们做选择。比如你要写一个任务调度器任务按提交顺序执行那队列就是天然的选择你要做括号匹配后进的左括号要先被匹配掉栈就更合适。我刚开始写代码时经常把栈和队列的API搞混后来养成了一个习惯写一个新功能前先问自己一句“按什么顺序消费数据”。如果答案是先到先处理就去拿队列如果是后到的先处理就换成栈。这个习惯帮我避开了很多“功能写完但顺序不对”的返工。2. 队列的数组实现与循环队列从“空间浪费”到“取模解决”实现队列不能光靠口头描述最终还是要落到代码。最常见的学习路径是先拿数组实现一遍再用链表实现一遍中间绕不开循环队列。2.1 最朴素的数组队列哪里浪费了用数组实现队列最直白的做法是申请一个固定大小的数组再维护两个变量front 表示队首下标rear 表示队尾下标。入队操作写入 arr[rear]然后 rear 加一出队操作读出 arr[front]然后 front 加一。这个思路看起来没问题但有一个隐蔽的问题front 只会变大不会回退。假设数组长度是5你连续入队5个元素再连续出队5个元素此时 front5、rear5队列里确实没有元素了但数组里已经没有可以继续写入的下标。如果 rear 继续加数组就越界了。换句话说即便数组空间是空闲的只要下标单调递增这个队列就只能用一次。浪费的不只是一半空间而是“用过的位置永远无法复用”。这是初学队列时最容易踢到的铁板明明数组是空的入队却报越界。2.2 循环队列是怎么把浪费的空间找回来的循环队列的思路很直接既然下标到头了还得到回绕那就让下标始终在 0 到 n-1 之间循环走到 n-1 之后下一个位置回到 0。具体做法是把下标移动方式从 front、rear 改成rear (rear 1) % capacityfront (front 1) % capacity取模运算就是那个“回绕”的关键。举个例子capacity5当前 rear4再入队一个新元素rear 变成 (41)%50下标回到了数组开头。之前被出队留下来的空位就能重新被使用。循环队列的容量判断也很关键。这里有一个约定俗成的做法为了区分“队空”和“队满”通常会浪费一个数组空间也就是说逻辑上只存放 capacity-1 个元素。判断条件写成队空front rear队满(rear 1) % capacity front为什么要留一个空位因为如果允许数组完全填满那么 rear 和 front 最终会相同而初始空队时两者也相同一个条件就对应两个含义程序无法区分当前到底是空还是满。留出一个空位后“满”的状态下 front 和 rear 永远隔一个位置歧义就消除了。2.3 边界条件判空判满不能靠拍脑袋我在初学阶段踩过这样一个坑循环队列容量设成5我试图塞满5个元素结果运行到第5个时提示“队满”百思不得其解。后来才意识到这个队列逻辑上只能存储 capacity-14 个元素因为用了“留一个空位”的方案。如果你不愿意浪费那个空位也有替代办法额外维护一个 size 变量记录当前队列里的元素个数。size0 就是空sizecapacity 就是满。代价是每次入队出队都要多维护一个计数多一次判断而且在多线程场景下size 的同步本身就是额外的复杂度。我的建议是学习阶段两种写法都练一遍但项目里优先用简单的“留空位”方案。状态变量每多一个出错的可能性就多一分。代码里少一个需要维护的字段就少一个需要解释的边界。3. 链表版队列指针怎么指才能把入队出队都做到 O(1)数组实现队列有着固定的容量上限如果想获得更灵活的扩展能力可以考虑使用链表。链表队列也不是一上来就顺理成章的它有一个微妙的指针设计问题。3.1 为什么数组和链表的“头尾指针”指法不一样稍微接触过链表的人应该都写过单向链表的节点typedef struct Node { int data; struct Node *next; } Node;如果只用链表实现栈只需要一个 head 指针插入和删除都在头部完成时间复杂度都是 O(1)。但同样的思路直接搬来做队列就会出问题如果只有一个 head 指针队尾插入需要从头遍历到尾时间复杂度是 O(n)出队虽然可以从头删O(1)但入队拖了后腿。为了让入队也变成 O(1)链表队列需要维护两个指针head 指向队首tail 指向队尾。入队时操作 tail出队时操作 head各干各的互不干扰两个操作才能同时做到 O(1)。我当时重新动手实现了一遍之后才发现数据结构的“指针设计”往往是由你要压平哪个操作的成本来决定的。先想清楚哪个操作要快再去设计指针比照着别人的代码抄一遍有用得多。3.2 链表队列的完整推演链表队列的入队过程可以这样写void enqueue(Node **head, Node **tail, int value) { Node *newNode (Node *)malloc(sizeof(Node)); newNode-data value; newNode-next NULL; if (*tail ! NULL) { (*tail)-next newNode; } else { *head newNode; // 原本队列为空队首要同步指向新节点 } *tail newNode; }出队过程int dequeue(Node **head, Node **tail) { if (*head NULL) { // 队空 return -1; } Node *temp *head; int value temp-data; *head (*head)-next; if (*head NULL) { *tail NULL; // 队首变空说明整个队列清空tail也必须重置 } free(temp); return value; }这里最容易漏掉的是出队后发现链表空了tail 还指向已经被释放的旧节点。下一次入队时如果直接对 *tail 的 next 操作就会走到一个野指针上程序崩溃都算运气好的更隐蔽的是内存被错误改写。初学链表队列时我只更新 head、忘了 tail调试了很久才发现是 tail 的问题。后来我把出队逻辑固定成“先更新 head再判断 head 是否为 NULL若是则 tail 也置 NULL”这个坑才被彻底填平。养成固定习惯之后这类问题基本不会再犯。3.3 数组队列和链表队列的选型对比对比项循环数组队列链表队列内存分配预先分配固定大小可能浪费或不够按需申请节点扩展灵活扩容成本需要搬移数据天然支持无需搬移节点开销无额外节点指针每个节点带一个 next 指针适用场景队列大小可预估追求低开销队列大小不确定请求规模波动大缓存友好度连续内存访问相对较高节点分散相对较低实际操作里我的选择标准是如果我能预估最大排队量就用循环数组队列如果完全没法预估担心溢出就用链表队列。两类方案各有各的适用场景没有绝对优劣。还有一个容易被忽略的细节是如果单条消息体很大链表每节点新增一个指针的开销可能无所谓但如果消息体本身很小而数据量巨大指针开销就会变得非常显著。4. 队列不只是考题三种真实场景里的队列队列如果不是为了解决实际问题的它就是一道考试题。但真实世界里队列用得比想象中广泛得多这里挑三个典型场景展开讲。4.1 消息队列生产者和消费者怎么通过“队”松耦合后台系统里有一种经典组合一个服务负责产生任务另一个服务负责处理任务。如果两个服务以同步调用的方式直接相连生产者每产生一个任务就必须等待消费者处理完才能继续效率低而且两个服务强耦合消费者出问题时生产者也很容易被拖垮。中间的解决方案就是消息队列。生产者把任务写入队列尾部消费者从队列头部取任务两者之间不需要直接见面。生产慢、消费快的时候队列处于空闲状态生产快、消费慢的时候队列暂时积累消费者按自己的节奏处理。队列在这里起到了缓冲和削峰的作用。我之前做过一个模拟项目X里面用队列做了一个任务分发模块一下子就把原来耦合的调用打散成两条独立的流水线后续调试起来轻松很多。这个思路放到生产环境也通用只要存在“上游产出数据、下游处理数据”的关系中间垫一个队列往往能显著提升系统的稳定性和可维护性。4.2 广度优先搜索为什么层级遍历非队不可图论和树结构里广度优先搜索BFS是数据结构课程绕不开的话题实现 BFS 几乎默认要使用队列。BFS 的思路是从起点出发先把下一层的所有邻居节点全部入队再依次处理下一层的节点每处理一个节点又把自己的邻居入队。如果使用栈你会不断深入一条路径走到尽头再回头那就是深度优先只有队列的先进先出才能保证先发现的节点先被扩展一层一层往外推进。我在做某迷宫寻路 Demo 时用队列实现了 BFS。输入一张二维地图起点终点固定BFS 不仅能找到路径还能天然保证是最短路径因为每一层扩散一定先到达终点。这个过程让我彻底理解了为什么教材里反复强调“BFS 用队列DFS 用栈”——不是规定而是两种数据结构的特性刚好匹配两种搜索策略。4.3 限流与滑动窗口用队列“量”流量限流场景里有个经典算法叫漏桶请求像水流一样进入队列队列按照固定速率处理请求处理能力之外的部分先在桶里排队。如果队列满了还继续来请求就直接拒绝以保护下游服务。另一个基于队列思路的限流方法是滑动窗口把时间划分为多个小窗口用队列记录每一个请求的时间戳。窗口移动时把过期的时间戳从队首弹出再统计窗口内的请求数。仔细看就能发现这依然是一个先进先出的结构——时间戳按产生顺序入队过期的部分按顺序被移除。这些场景的共同特征是数据到来顺序和消费顺序一致先到的先处理处理不完的先排队。只要出现这三个特征就应该优先想到队列。判断一个场景用不用队列不用硬背算法抓住“顺序一致性”这个点就够了。5. 队列的三个变种优先队列、双端队列、阻塞队列基础队列之外实际工程里更常见的其实是队列的变种。它们和标准队列长得相似但各自解决了不同的问题。5.1 优先队列打破了 FIFO靠的是堆优先队列PriorityQueue名字里带“队列”但出队时并不保证先进先出而是每次弹出优先级最高的元素。这个优先级可以是数值大小也可以是自定义的比较规则。实现优先队列的常见底层结构是二叉堆。使用堆的理由是入队插入和出队弹出最值都能做到 O(log n) 的时间复杂度比“每次都把所有元素重新排序”要快很多。如果直接用数组加排序每次出队都要 O(n log n)数据量一大就没法看了。第一次接触优先队列时我有个疑问这种东西明明不遵守 FIFO还叫队列吗后来想通了它和队列的相同点是“数据有进有出、操作集中在某一端”不同点只是出队的选取规则。工程上“优先队列”这个命名用了很多年约定俗成大家理解它的行为即可。5.2 双端队列 Deque头尾都能进Deque 的全称是 Double-Ended Queue支持在队首和队尾都进行插入和删除。如果你只在队尾插入、队首删除它就是普通队列如果只在队尾插入和删除那它本质上就是栈。它像一把“瑞士军刀”把队列和栈的能力合到一起。这个灵活性让 Deque 在不少算法题里很占优势最典型的就是滑动窗口最大值问题。它既能从队尾淘汰“不可能再成为最大值的元素”也能从队首淘汰“已经滑出窗口的元素”只用一次遍历就能求出每个窗口的最大值复杂度压到 O(n)。实现 Deque 可以用循环数组也可以用双向链表。双向链表更直观每个节点有 prev 和 next 两个指针首尾插入和删除都是 O(1)。我自己写实现的时候比较偏好双向链表因为它不需要考虑数组扩容的边界思路更清晰。5.3 阻塞队列多线程下的“安全阀门”单线程场景里队列的入队和出队是顺序执行的。但多线程环境下如果生产者线程和消费者线程同时读写同一个队列就会出现数据不一致、重复消费、内存错乱等并发问题。阻塞队列BlockingQueue解决了这个问题入队时如果队列满了生产者线程会被阻塞直到消费者腾出空间出队时如果队列空了消费者线程会被阻塞直到有新的元素进来。这个机制让生产者和消费者之间天然实现了节奏同步不会出现一边疯狂生产把内存撑爆、另一边干等的情况。Java 里有一个实现叫 ArrayBlockingQueuePython 的标准库 queue 模块也提供了类似的功能。使用这类成熟实现比自己在队列外面加锁要安全得多。这也是“不要自己造队列”的一个重要原因后面我会再展开说。6. 手写队列踩过的坑和测试时容易被忽略的边界最后这部分我想把实际排错过程中积累的经验集中分享出来。它们不一定写在教材里但出现频率极高。6.1 循环队列的“留一个空位”约定写过循环数组队列的人应该都经历过一个问题这个队列到底能装几个元素答案是取决于你选择的方案。如果采用“留一个空位”方案物理容量为 n 的数组逻辑容量是 n-1如果引入 size 计数逻辑容量可以到 n。我一度用 size 记录元素个数觉得这样多存一个元素很划算。后来发现每次入队出队都要维护 size判空判满要同时比较 size 和下标代码并不复杂但测试用例却增加了一倍。后来换回“留空位”方案少了这个状态判空判满只需要比较两个下标代码反而更简单。我的体会是小项目里尽量少维护状态。多数时候代码最简单的版本就是最不容易出错的版本为一个元素的空间去引入额外的状态变量不太值得。6.2 指针移动的先后顺序错一步就丢数据入队的正确顺序是先写数据再移动 rear 指针。但有的写法会反过来先移动指针再写数据导致队首指向的位置跳过了一个已入队元素出队时读出的数据永远是第二个或更后面的第一个元素被永久丢弃。这个错误在数组队列和链表队列里都可能出现。排查方法很朴素写一组最基础的测试用例依次入队 1、2、3再连续出队检查结果是不是 1、2、3。这个测试必须最先跑不用等功能全部写完再测。很多时候bug 在刚写完入队函数时就能暴露出来早一点写测试后面就能少一点排查成本。6.3 用哨兵节点简化链表队列链表队列最烦人的地方是空队与非空队的状态切换入队时队列为空要额外设置 head出队后队列变空要额外重置 tail。这些分支写多了代码就会很碎。一个常用的简化技巧是引入哨兵节点dummy node让 head 指针永远指向一个不存实际数据的节点真正的队首元素是哨兵节点的 next。这样无论队列是否为空head 都不会变成 NULL入队和出队的逻辑都少了很多 if 分支。我第一次写链表队列时没有用哨兵代码里到处是“如果 head 为 NULL 怎么办”的判断测试时也容易漏掉某个分支。引入哨兵之后代码明显清爽不少出错率也下来了。这个技巧不仅适用于队列很多涉及链表边界处理的结构都可以借鉴。6.4 真实项目里“不要自己造队列”的原因说了这么多手写队列的经验最后想给一个实事求是的原则学习阶段一定要手写越细致越能理解底层但生产项目里能用标准库就用标准库。Java 的 Deque、C 的 std::queue、Python 的 queue 模块、Go 的 channel这些实现经过大量场景的检验边界处理比个人手写的版本可靠得多。我在某个系统里为了“灵活可控”自己实现了一个线程安全队列加锁、扩容、判空、判满全都自己写表面看功能完备但在高并发下偶尔会出现“莫名其妙丢数据”的情况。排查了很久最后发现问题出在出队时先移动了 head 指针再读取数据竞态窗口里别的线程已经改掉了指针。如果直接用标准库的阻塞队列这个坑完全可以避免。所以我的建议是练习阶段把数组队列、循环队列、链表队列都手写一遍把边界条件都测明白真实项目里把“自己造队列”当成最后的选择优先用成熟组件。数据结构本身是工具工具的意义在于帮助解决问题而不是给人增加维护成本。手写队列的经验说到底就是三件事把入队、出队两个操作的时间复杂度都想清楚把判空、判满的边界条件测到位把多线程环境下的安全交给成熟方案。能做到这三点再用队列时心里会踏实很多。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号