队列数据结构全解析:从FIFO到循环队列、双端队列与优先队列实战

发布时间:2026/10/11 2:37:30

队列数据结构全解析:从FIFO到循环队列、双端队列与优先队列实战 很多人第一次接触“队列”这个数据结构通常是在学完数组和链表之后。我当时也是书上看了一眼“先进先出”觉得简单不就是排队吗真到自己动手写代码的时候才发现一个小小的queue从实现到使用能挖出一堆门道。有同学把“队列”简称为“队”然后问我“数据结构队和queue到底有什么区别”其实它俩就是同一个东西queue就是队列“队”是中文叫法“queue”是英文术语说的都是这种只允许在一端插入、在另一端删除的线性表。这篇文章我想把队列这件事彻底讲清楚。不光是背概念还包括它为什么这么设计、有哪些实现方式、在真实项目里解决什么问题、以及我踩过的坑。不管你是刚开始学数据结构的学生还是工作中需要处理任务排队的开发者都应该能从中拿到点干货。1. 先把“队”这个概念掰扯清楚1.1 “队”和Queue到底是不是一回事在数据结构语境里“队”就是队列也就是queue。很多中文教材会写“队列”日常口语里大家懒得说两个字就叫“队”。这本身没什么问题但在看英文资料时如果不知道queue对应中文的“队列”就会出现“数据结构队和queue是什么关系”这样的疑问。队列的核心规则只有一条从队尾rear入队从队头front出队。第一个进来的元素永远第一个被处理就像食堂打饭一样先来的人先打到菜后来的排在后面。这个规则有个专业术语叫FIFOFirst In First Out先进先出。跟它经常一起出现的还有一个结构叫栈stack规则是后进先出LIFO像一摞盘子后放的反而先拿。这两个结构是线性数据结构里的一对“反义词”也正因为它们规则简单才最适合用来练习对数据结构的理解。队列在生活中随处可见医院挂号、奶茶店取餐、打印机任务排队都是队列模型。理解这个模型的关键不在于“排队”这个动作本身而在于“公平性”和“顺序性”——先到的先服务谁也别想插队。1.2 队列的接口与性能契约一个队列对外提供的核心操作其实很少就是那么几个入队enqueue / push / offer把元素放到队尾出队dequeue / pop / poll把队头元素取走并删除查看队头front / peek读取队头元素但不出队判空isEmpty队列里有没有元素获取大小size / len当前队列里有多少元素代码层面不同的语言给这些操作起了不同的名字。C的std::queue用push、pop、frontJava的Queue接口用offer、poll、peekPython的collections.deque用的是append和popleft。名字虽然不一样语义都一样。有个容易被忽略的点是“性能契约”入队和出队都应该是O(1)时间复杂度。为什么因为队列的本质是“流水线”如果入队或出队时要搬移一批元素那队列就退化成数组的复制操作了完全失去意义。后续我们看各种实现方式本质上都是在想办法让这两个核心操作保持O(1)。这里多说一句队列的“队头”和“队尾”并不是固定的物理位置只是在逻辑上约定了一个方向。这也引出了下文的实现问题——用什么方式存储元素才能让两端操作都高效。2. 从零实现一个队列三种实现方式怎么选2.1 用数组实现简单直接但有个隐藏坑初学者最容易想到的实现方式就是用数组加两个下标front指向队头rear指向队尾。入队时在rear位置写入元素rear向右移动出队时读取front位置的元素front向右移动。这个方案在队列刚起步时挺顺手但用不了多久就露馅了。假设数组长度是5连续入队5个元素后rear已经指到数组末尾此时再想入队第6个元素哪怕front之前已经出队了好几个元素数组前半部分是空着的程序也会认为“队列满了”。这就是经典的“假溢出”问题——不是真满而是rear到头了。有人会说那出队时把后面的元素往前搬不就行了确实可以但出队操作就会变成O(n)。每次出队都要搬动剩余所有元素队列越长约慢这在性能敏感的代码里是完全不能接受的。所以数组直接实现队列需要配合“环形”的思想来解决这就是循环队列。2.2 循环队列把数组掰成一个环循环队列的思路很简单不再把数组当作一条直线而是当成一个首尾相接的环。rear移动到数组末尾后下一个位置绕回下标0。实现时要维护两个指针front指向队头rear指向下一个可写入的位置。入队时让rear后移一位出队时让front后移一位都用取模运算实现class CircularQueue: def __init__(self, capacity: int): self.capacity capacity self.data [None] * capacity self.front 0 # 队头下标 self.rear 0 # 下一个入队位置 def is_empty(self) - bool: return self.front self.rear def is_full(self) - bool: return (self.rear 1) % self.capacity self.front def enqueue(self, value): if self.is_full(): raise OverflowError(队列已满) self.data[self.rear] value self.rear (self.rear 1) % self.capacity def dequeue(self): if self.is_empty(): raise IndexError(空队列不能出队) value self.data[self.front] self.data[self.front] None self.front (self.front 1) % self.capacity return value def peek(self): if self.is_empty(): raise IndexError(空队列不能查看队头) return self.data[self.front] def __len__(self): return (self.rear - self.front self.capacity) % self.capacity注意这里有个特别容易踩坑的设计判满条件是(rear 1) % capacity front也就是说循环队列故意牺牲了一个存储位置用来区分“空”和“满”。如果不牺牲这一个位置判空条件front rear和判满条件也是front rear两个状态就撞车了无法区分。你当然也可以不牺牲空间改用额外字段记录元素个数或者再加一个标志位表示“当前是否满”。但最常见的写法还是牺牲一格因为它够简单也不用额外维护状态。2.3 链表实现按需使用灵活但开销更大链表实现队列不需要担心“满”的问题因为元素是动态创建的内存够用就能继续加。核心思路是维护两个指针head指向队头节点tail指向队尾节点。入队时在tail后面挂新节点出队时从head取节点class ListNode: def __init__(self, value): self.value value self.next None class LinkedQueue: def __init__(self): self.head None self.tail None self.size 0 def enqueue(self, value): node ListNode(value) if self.tail is None: self.head self.tail node else: self.tail.next node self.tail node self.size 1 def dequeue(self): if self.head is None: raise IndexError(空队列不能出队) value self.head.value self.head self.head.next if self.head is None: self.tail None self.size - 1 return value def is_empty(self) - bool: return self.head is None出队时要特别注意如果head向后移后变成了None要记得把tail也置为None否则队列空时tail还指向一个已经删除的节点后面再入队就会出逻辑错误。三种实现方式对比一下实现方式入队复杂度出队复杂度内存特征适用场景普通数组O(1)尾部O(n)搬移连续内存几乎不用循环数组O(1)O(1)连续内存固定容量容量可预估、追求性能链表O(1)O(1)离散内存按需分配容量不确定、需要频繁扩容实际工程里官方库基本都替你做好了选择。Java的ArrayDeque、Python的collections.deque本质上都是环形缓冲区的动态数组实现兼顾性能和容量伸缩。自己手写循环队列主要发生在做题、面试或者某些内存受限的嵌入式场景。3. 队列在真实项目里解决什么问题3.1 缓冲解耦生产者和消费者的桥梁队列最大的价值不是“排队”这个形式而是它能把“谁产生数据”和“谁消费数据”这两个事情拆开。举个例子。一个订单系统每秒能接收500个请求但数据库每秒只能写入200条。如果让请求直接打数据库数据库瞬间被压垮。这时候在中间放一个队列请求先入队后台程序按自己能力从队里取数据慢慢处理系统的整体稳定性立刻提升。这就是“削峰填谷”。操作系统里到处是这样的队列CPU就绪队列保存等待执行的进程I/O请求队列保存等待磁盘响应的请求打印机任务队列保存多个用户提交的打印任务。这些场景有个共同特征生产数据的速度和处理数据的速度不一致。队列作为缓冲地带让两者不必互相等待也不必强行同速。理解了“缓冲解耦”你再看很多后端系统里的任务队列、线程池都会有一种豁然开朗的感觉——它们本质上都是同一个模型。3.2 BFS遍历算法领域里的队列主场要说队列在算法里最经典的应用一定是BFS广度优先搜索。树的层序遍历、图的层级扩散、迷宫的最短路径全都要靠队列。BFS为什么非队列不可因为它的遍历顺序是“先发现先处理”。我们从一个起点出发它周围的邻居先进入视野这些邻居应该比更远的节点先被访问。这种天然的“先来先服务”语义和队列的FIFO完全吻合。以二叉树的层序遍历为例def level_order(root): from collections import deque if not root: return [] result [] q deque([root]) while q: level_size len(q) level_values [] for _ in range(level_size): node q.popleft() level_values.append(node.value) if node.left: q.append(node.left) if node.right: q.append(node.right) result.append(level_values) return result这段代码里有一个关键细节每次进入一层时先取len(q)只处理当前这一层的节点数而不是无脑while循环。很多初学者只要少写这一步BFS就会变成层与层混在一起最后输出的就不是层序遍历结构了。这个细节在面到“二叉树的右视图”“二叉树的最大深度”这类题时同样适用。3.3 消息队列和数据结构的队列不是一回事我发现很多开发者会把“消息队列”和数据结构里的队列当成一个东西这是个很大的误区。数据结构队列是程序内存里的一段连续存储解决的是同一个进程内“数据如何组织”的问题。而消息队列比如常见的开源消息中间件是一个独立的系统组件解决的是“不同服务之间如何异步通信”的问题。消费者和生产者可能不在同一台机器上消息要经过网络传输、持久化存储、多副本复制这些都不是一个简单的FIFO结构能搞定的。它们之间的关系是消息队列中间件在底层实现中会用到各种数据结构来组织待投递的消息可能包含队列、优先级队列、索引结构等等。但你在业务代码里用消息中间件时关注的是“服务A发消息给服务B服务B稍后处理”而不是在内存里写一个队列对象。如果你写代码时遇到“这个业务该不该用消息队列”的困惑先想清楚问题是不是跨进程的、有没有削峰需求。如果只是进程内任务排队用一个线程安全的队列就够了如果涉及多个服务解耦、异步通知才需要考虑消息中间件。4. 队列家族的两员大将双端队列与优先队列4.1 双端队列两端都能操作滑动窗口的利器双端队列DequeDouble Ended Queue不遵守“一端进一端出”的规矩它允许在队头和队尾两端都进行插入和删除。这个能力救了很多算法题最典型的就是滑动窗口最大值。滑动窗口的原理是维护一个双端队列让队头始终保存当前窗口的最大值下标。新元素进窗口时把队尾所有比它小的元素都丢掉窗口滑动时如果队头下标已经滑出窗口就把它从队头弹出。这套操作如果不用双端队列就得用普通数组扫描每次窗口移动都是O(k)整个流程退化成O(nk)。用双端队列之后每个元素最多入队出队一次整体是O(n)效率立竿见影。Python里用起来也简单from collections import deque def max_in_windows(nums, k): dq deque() # 存下标队头是当前窗口最大值 result [] for i, val in enumerate(nums): while dq and nums[dq[-1]] val: dq.pop() dq.append(i) if dq[0] i - k: dq.popleft() if i k - 1: result.append(nums[dq[0]]) return result双端队列实现时一般也采用环形缓冲区或者双向链表。C标准库里的deque是分段连续存储Java的ArrayDeque则是不允许存null的环形数组。日常开发里如果你没有特殊的头部操作需求直接用语言自带的双端队列实现就好没必要自己去造。4.2 优先队列谁优先级高谁先出队列和优先队列就差一个定语FIFO按到达顺序出队优先队列按“优先级”出队。优先级高的元素即使后面来的也会被排在前面。优先队列的经典实现是二叉堆heap插入和删除的复杂度都是O(log n)比普通队列的O(1)要慢但换来的是“每次取到的一定是当前最该处理的元素”。它在算法里出场率极高求TopK、求数据流中位数、Dijkstra求最短路径核心都是优先队列。Python里用heapq模块操作的是普通列表只是把它当成堆用import heapq pq [] heapq.heappush(pq, (3, 普通任务)) heapq.heappush(pq, (1, 紧急任务)) heapq.heappush(pq, (2, 一般任务)) while pq: priority, task heapq.heappop(pq) print(task, priority)这种数据结构在业务中的典型场景是一个工单系统里VIP用户的问题要优先处理但如果后台来的全是VIP请求普通用户的请求也不能永远饿着。这时候可以结合“老化机制”随着等待时间增长逐步提升优先级这就是优先队列的进阶玩法。类型出队顺序核心复杂度典型场景普通队列按到达顺序O(1)任务排队、BFS双端队列两端都可操作O(1)滑动窗口、回文判断优先队列按优先级O(log n)TopK、最短路径、任务调度5. 实操示例用队列实现一个排队叫号系统5.1 需求拆解与整体设计纸上谈兵再多不如拿一个真实场景练练手。我之前在做一个模拟政务大厅的排队叫号系统时就遇到了最典型的队列应用场景这里把核心设计分享出来。需求其实不复杂用户取号后进入排队队列窗口叫号时从队头取一个号用户可以查看自己前面有多少人在等特殊情况某个窗口暂停服务刚叫到号的用户要重新排到队尾操作列表一列对应关系就很清楚了取号入队enqueue叫号出队dequeue查看前方人数查询队列长度重新排队再次入队这个系统在单机单线程的场景下直接用collections.deque就够了。它的append和popleft操作都是O(1)底层是环形缓冲区性能足够好。如果以后改成多个窗口并发叫号再换成线程安全的queue.Queue。5.2 核心代码与边界处理from collections import deque class TicketSystem: def __init__(self): self.queue deque() self.counter 0 def take_ticket(self): self.counter 1 self.queue.append(self.counter) print(f您拿到的号码是 {self.counter}前方还有 {len(self.queue) - 1} 人等待) return self.counter def call_next(self): if not self.queue: print(当前没有排队中的号码) return None current self.queue.popleft() print(f请 {current} 号到窗口办理) return current def requeue(self, ticket_no): self.queue.append(ticket_no) print(f{ticket_no} 号已重新排到队尾当前前方有 {len(self.queue) - 1} 人) def waiting_count(self): return len(self.queue)这个实现里有几个细节值得说明。首先是空队列判断。call_next方法里必须先判空否则对空队列调用popleft会直接抛IndexError。这在真实系统里很常见窗口闲下来了去队列里取号结果队列是空的。空队列取号不是程序bug而是一种正常业务状态所以要在代码里显式处理。其次是取号时打印“前方有多少人等待”这句。注意这里用的是len(self.queue) - 1因为自己刚入队也算一个元素实际上前面排队的人数要减掉自己。这种“差一”问题在队列业务里到处都是写代码时务必要把“当前自己是否在队列里”这个状态想清楚。再说说重新排队的实现。用户被叫到号之后如果窗口暂停需要把号码重新追加到队尾。这里有个业务判断在真实系统里重新排队可能不是简单的append而是要考虑“插队”还是“排尾”。我们为了体现FIFO的公平性选择的方案是老老实实排到队尾。如果你实现的是“过号作废”策略那么出队后直接丢弃就好不需要重新入队。如果你的场景是多线程并发——比如取号由前台线程负责叫号由窗口线程负责——那deque就不安全了。两个线程同时修改队列可能丢数据标准做法是直接改用queue.Queueimport queue class TicketSystemThreadSafe: def __init__(self): self.queue queue.Queue() self.counter 0 self.lock threading.Lock() def take_ticket(self): with self.lock: self.counter 1 ticket_no self.counter self.queue.put(ticket_no) return ticket_no def call_next(self): if self.queue.empty(): return None return self.queue.get()queue.Queue内部已经加了锁所以put和get在多线程下是安全的。锁只保护计数器避免两个线程同时拿到同一个号码。6. 实战中遇到的坑与排查思路6.1 循环队列的“差一错误”是经典Bug来源循环队列看似简单但实现起来稍不留神就会写错尤其是判满条件。我之前写过一个固定容量的循环队列就吃过这样的亏入队时没有先判满结果rear绕了一圈之后把front还没取走的元素覆盖掉了数据莫名其妙丢失查了很久。排查这个Bug的方法其实并不复杂。在入队和出队的关键路径上打印front和rear的值然后逐一步模拟。比如容量为5的队列入队5个元素front0、rear0这时候再入队第6个元素(rear 1) % capacity等于1不等于front所以能继续写入结果rear变成1紧接着覆盖了下标0的数据。问题就出在“到底允许存几个元素”上。循环队列的经典实现允许存储capacity - 1个元素。如果你想让容量为5的队列能存5个元素就得改用“size计数”方案或者额外加一个标志位记录“最后一次操作是入队还是出队”。做这道题时建议先写几个边界用例空队列出队应该报错队列满后继续入队应该报错入队N个、出队N个之后再次入队应该一切正常先入队到满再出队到空循环反复数据不能丢能一次性通过这四个用例循环队列才算写对了。6.2 并发环境别让队列自己裸奔队列本身只是数据结构它不保证并发安全。把deque放到多线程环境里不加锁直接append和popleft数据错乱只是时间问题。我见过一次生产事故多个消费者线程同时从队列中取任务偶尔会有两个线程拿到同一条任务原因就是出队操作不是原子的。解决并发队列问题有三条路线按场景选使用语言自带的线程安全队列比如Python的queue.Queue、Java的ConcurrentLinkedQueue自己加锁保护入队出队操作使用无锁队列lock-free queue适合追求极致性能的场景但实现复杂度高这里特别想提醒一句无锁队列不是银弹。它的实现依赖CAS等原子操作逻辑一旦写错排查难度远高于加锁方案。普通业务场景老老实实用自带的线程安全队列就好性能足够了。6.3 做题时的栈队混淆如何快速识别最后一个坑是思维层面的。很多人在刷题时会搞混栈和队列尤其是遇到“用栈实现队列”和“用队列实现栈”这两道经典题时。识别方法非常简单记住一个原则——“进出同端是栈进出异端是队列”。栈是单口容器进出都在栈顶队列是双口容器一端进一端出。做题前先停下来问自己当前这道题要求的操作是“同端进出”还是“异端进出”确定了再动手。“用两个栈实现队列”的做法是入队时直接往栈A压出队时如果栈B是空的就把栈A所有元素倒进栈B再从栈B弹出顶部。这本质上是用两个LIFO拼一个FIFO。“用两个队列实现栈”则反过来每次入栈时把元素放到非空队列的队尾出栈时把前面所有元素搬移到另一个队列剩下最后一个元素出队。这两题的价值不在于记住代码而在于理解“数据结构的选择决定操作复杂度”。如果你对队列的FIFO语义足够敏感看到这类题时的第一反应应该是“怎么把顺序翻转过来”而不是死记硬背书上的解法。我在实际项目里用队列的次数比栈多得多。几乎所有“请求-处理”的中间环节都可以用队列来承载爬虫抓取URL的待抓取队列、异步任务处理器、操作日志的写入缓冲。每次我只需要保证生产者把数据交到队列、消费者按自己的节奏处理系统就能在不改动总体结构的情况下把两个模块之间的耦合降到最低。最后分享一个小习惯每次写完队列相关代码我都会顺手跑一遍空队列和满队列的边界测试再模拟一次“生产快于消费”和“消费快于生产”两种节奏。队列这东西本身不复杂但正因为简单细微的错误反而容易被忽略。养成边界测试的习惯后很多隐蔽问题可以在写代码的阶段就暴露出来而不是等上线了再去救火。
延伸阅读

更多相关文章

2026/10/11 2:37:30

校园便利平台毕设全解析:SpringBoot+Vue从零到可交付

最近帮一个学弟把“校园便利平台”这类型的老项目从零到一重新梳理了一遍,顺手把源码、SQL 脚本和接口文档全部整理成了一套可以直接跑起来的毕设级交付物。说实话,这类题目在 Java Web 毕设里非常典型,表面看是“一个 SpringBoot 后端 一个…

2026/10/11 2:37:30

SpringBoot+Vue物流管理系统:从选题到答辩的全栈毕设实战指南

最近后台私信里被问最多的就是毕业设计选题,十个里有七八个都在问有没有“能跑、好写、好答辩”的Java项目。的确,毕设踩坑踩在选题上,可就太冤了。今天我要聊的这个SpringBootVue物流管理系统,就是我自己带过的学生里&#xff0c…

2026/10/11 2:37:30

瑞吉外卖源码跑通指南:Spring Boot与Redis缓存实战

简介:面向正在学习SpringBoot、MyBatis-Plus的Java开发者,这份源码项目还原了一个完整外卖平台的核心业务闭环,涵盖用户注册、登录、下单、支付等关键流程,并针对真实场景做了多处优化:手机短信登录改为邮箱验证&#…

2026/10/11 3:37:37

Java并发线程安全与可见性:从JMM到volatile实战解析

在并发编程这块待久了,你会发现真正让人头疼的不是死锁,也不是线程池参数,而是一些看起来“明明没问题”的代码,跑起来却像中了邪一样随机出错。我印象最深的一次是在排查一个库存扣减的偶发超卖问题:业务逻辑加了对账…

2026/10/11 3:37:37

C++命令模式实战:从撤销重做到任务队列

提起“命令模式”(Command Pattern),很多人的第一反应是设计模式书里那张UML图:Command、ConcreteCommand、Receiver、Invoker,四个框框几条箭头,看着挺抽象。但真正在C工程里把它用顺手之后,你…

2026/10/11 3:37:37

TOA测距与最小二乘伪逆解算:冗余锚点下的MATLAB定位仿真

在定位技术这个圈子里摸爬滚打这几年,我越来越觉得一个现象挺有意思:很多刚接触定位算法的朋友,一上来就盯着“三边定位”这个名字,以为它只能靠三个锚点干活。但实际上,当你的场景里铺了成百上千个锚点——比如室内定…

2026/10/11 3:37:37

外卖学习第三天 39/200

外卖学习第三天 1、补充第二天的公共字段自动填充遗留下的问题/*** 切入点* */Pointcut("execution(* com.sky.mapper.*.*(..)) && annotation(com.sky.annotation.AutoFill)")public void autoFillPointCut(){}/*** 前置通知,在通知中进行公共字…

2026/10/11 3:37:37

9轴IMU姿态解算:卡尔曼滤波算法设计与Matlab实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑