队列与约瑟夫环:顺序队列、链式队列及环形逻辑的C++实现

发布时间:2026/10/20 21:08:09

队列与约瑟夫环:顺序队列、链式队列及环形逻辑的C++实现 1. 队列与约瑟夫环从线性结构到环形逻辑的完整实现队列这个结构很多人第一次学的时候觉得“这不就是排队吗”然后写了个数组加两个下标就交差了。但真到用的时候比如做任务调度、消息缓冲、BFS 遍历才发现边界处理、假溢出、内存管理这些坑一个比一个深。这篇文章把顺序队列、链式队列和约瑟夫环三个东西串在一起讲因为它们本质上是一条线上的前两个解决的是“先进先出”的存储问题约瑟夫环则是队列思想在环形逻辑上的经典应用。C 实现代码可以直接抄但更重要的是把每个设计决策背后的原因说清楚。先说说这三个东西各自解决什么问题。顺序队列用一段连续内存存数据靠队头和队尾两个指针控制出入优点是访问快、缓存友好缺点是容量固定而且朴素实现会有“假溢出”——明明前面有空位但因为队尾到头了就不能再插入。链式队列用节点串联动态扩容不存在假溢出但每个节点多一个指针开销缓存局部性差一些。约瑟夫环是一个数学游戏n 个人围成一圈从第 k 个人开始报数报到 m 的人出列然后从下一个人继续直到所有人出列。用队列模拟这个过程非常直观——出队的人如果没报到 m 就重新入队报到 m 就彻底出队。适合谁看如果你正在学数据结构课本上的代码跑不通或者看不懂这篇文章能帮你把每个细节补齐。如果你已经工作了需要手写一个无锁队列或者环形缓冲顺序队列的循环数组实现就是基础。如果你在准备面试约瑟夫环是高频题但面试官更想听的是你为什么用队列而不是链表直接模拟以及时间复杂度怎么分析。我见过太多人学队列的时候只记住“先进先出”四个字结果写出来的代码在队空、队满、循环回绕这些地方全是 bug。下面从设计思路开始一步步把这三个结构拆开揉碎。2. 顺序队列的设计与实现细节2.1 为什么需要循环队列朴素顺序队列用front和rear两个下标front指向队头元素rear指向队尾元素的下一个位置。入队时rear出队时front。问题很快就来了假设容量是 5依次入队 5 个元素rear变成 5此时队满。然后出队 2 个元素front变成 2rear还是 5。这时候队列里实际只有 3 个元素但rear已经到达数组末尾再入队就会越界。这就是假溢出——数组前面明明有 2 个空位但因为rear不能回绕空间被浪费了。解决办法就是把数组在逻辑上弯成一个环。rear到达容量上限后通过取模运算回到下标 0。这样只要队列里元素个数小于容量就一定能插入。循环队列的核心公式就两个入队时rear (rear 1) % capacity出队时front (front 1) % capacity。但取模带来一个新问题怎么判断队空和队满如果不加处理front rear既可能是空也可能是满。常见方案有三种一是牺牲一个存储单元当(rear 1) % capacity front时认为队满这样队列实际最多存capacity - 1个元素二是额外维护一个size变量记录当前元素个数size 0为空size capacity为满三是用一个flag标记最后一次操作是入队还是出队。我推荐第二种因为size变量不仅解决了判空判满还能 O(1) 时间返回队列长度非常实用。2.2 循环队列的 C 实现下面是一个完整的循环队列模板类用size变量管理状态支持泛型。template typename T class CircularQueue { private: T* data; int capacity; int front; int rear; int count; public: explicit CircularQueue(int cap 10) : capacity(cap), front(0), rear(0), count(0) { data new T[capacity]; } ~CircularQueue() { delete[] data; } bool empty() const { return count 0; } bool full() const { return count capacity; } int size() const { return count; } bool enqueue(const T value) { if (full()) return false; data[rear] value; rear (rear 1) % capacity; count; return true; } bool dequeue(T out) { if (empty()) return false; out data[front]; front (front 1) % capacity; --count; return true; } bool peek(T out) const { if (empty()) return false; out data[front]; return true; } };这段代码有几个关键点值得展开。第一构造函数用explicit防止隐式转换比如CircularQueueint q 10;这种写法会被编译器拒绝必须写成CircularQueueint q(10);避免误用。第二enqueue和dequeue返回bool而不是void调用方可以判断操作是否成功这在生产代码里很重要——你永远不知道调用方会不会在队满时还硬塞数据。第三peek是const成员函数因为它不修改队列状态这样const CircularQueue也能调用。注意如果 T 是复杂对象data[rear] value会触发拷贝赋值。对于大对象可以考虑用移动语义data[rear] std::move(value)但要注意value是const引用时不能移动。实际项目中如果队列存的是大结构体建议存指针或std::unique_ptr。2.3 容量选择与性能考量循环队列的容量怎么定如果太小频繁队满导致入队失败如果太大浪费内存。一个经验法则是根据生产者和消费者的速率差来估算。假设生产者每秒产生 1000 个任务消费者每秒处理 800 个那么队列会以每秒 200 个的速度增长。如果希望队列能缓冲 10 秒的突发流量容量至少是 2000。但这是理想情况实际还要考虑内存限制和缓存行大小。从性能角度看循环队列的入队和出队都是 O(1)而且因为数据在连续内存上CPU 缓存命中率高。但取模运算%在有些架构上比较慢可以用条件判断替代rear (rear 1 capacity) ? 0 : rear 1;。现代编译器通常会把% capacity优化成位运算前提是capacity是 2 的幂。所以如果你对性能有极致要求把容量设为 2 的幂比如 1024、4096这样取模就变成了 (capacity - 1)一条指令搞定。还有一个细节front和rear用int还是size_t如果队列容量不会超过INT_MAX用int没问题。但如果容量可能很大用size_t更安全。不过size_t是无符号的做减法时要小心下溢。我一般用int因为队列容量超过 20 亿的场景几乎不存在而且int的运算更快。3. 链式队列的适用场景与实现3.1 链式队列 vs 顺序队列怎么选链式队列用节点存储数据每个节点包含数据和指向下一个节点的指针。队头指针front指向第一个节点队尾指针rear指向最后一个节点。入队在rear后面接一个新节点并更新rear出队删除front节点并更新front。因为节点是动态分配的队列长度没有上限受限于内存也不存在假溢出。那什么时候用链式队列一是队列长度变化很大事先无法预估最大容量二是元素本身很大移动或拷贝成本高用链式结构只需要改指针三是需要频繁在队列中间插入或删除虽然标准队列不支持但可以扩展。反过来如果队列长度稳定或者对缓存性能要求高顺序队列更合适。我做过一个网络包处理的模块包大小平均 1.5KB队列深度在 1000 左右波动用循环队列预分配 2048 个槽位性能比链式队列好 30% 以上因为链式队列每次入队都要new一个节点内存分配开销不可忽视。3.2 链式队列的 C 实现template typename T class LinkedQueue { private: struct Node { T data; Node* next; Node(const T d) : data(d), next(nullptr) {} }; Node* front; Node* rear; int count; public: LinkedQueue() : front(nullptr), rear(nullptr), count(0) {} ~LinkedQueue() { while (front) { Node* tmp front; front front-next; delete tmp; } } bool empty() const { return front nullptr; } int size() const { return count; } void enqueue(const T value) { Node* node new Node(value); if (rear) { rear-next node; } else { front node; } rear node; count; } bool dequeue(T out) { if (empty()) return false; out front-data; Node* tmp front; front front-next; if (!front) rear nullptr; delete tmp; --count; return true; } bool peek(T out) const { if (empty()) return false; out front-data; return true; } };这段代码里enqueue不需要判断队满因为链表理论上可以一直加节点直到内存耗尽。dequeue里有一个容易忽略的细节当出队后front变成nullptr时必须把rear也置空否则rear会指向一个已经被delete的节点下次入队时rear-next node就是访问野指针程序直接崩溃。这个 bug 我在早期项目中踩过调试了半天才发现。析构函数里用循环逐个释放节点不能用递归否则队列很长时会栈溢出。另外如果 T 是复杂类型new Node(value)会调用拷贝构造可以考虑完美转发和移动语义来优化但那是另一个话题了。3.3 内存管理与异常安全链式队列最大的风险是内存泄漏。每次enqueue都new一个节点如果dequeue时忘记delete或者异常路径上没有释放内存就会慢慢涨上去。C 里更现代的做法是用std::unique_ptrNode管理节点这样出队时unique_ptr自动释放不需要手动delete。但unique_ptr的链式结构在遍历和析构时会有递归问题需要小心处理。另一个问题是异常安全。如果new Node(value)抛出std::bad_alloc队列状态不变这是强异常安全保证。但如果T的拷贝构造函数抛异常new表达式会释放已分配的内存也不会泄漏。所以标准写法是安全的。但如果你在enqueue里做了多步操作比如先改rear-next再new那就可能出问题。我的建议是先把所有可能抛异常的操作做完再修改队列状态。提示如果队列会被多个线程同时访问链式队列需要加锁。但加锁的粒度很关键——锁整个队列最简单但并发度低。可以尝试锁分离入队锁rear出队锁front但要注意空队列时的竞争条件。无锁队列是另一个层次的话题需要原子操作和内存屏障这里不展开。4. 约瑟夫环队列思想的环形应用4.1 问题建模与队列模拟约瑟夫环的描述是n 个人围成一圈编号 1 到 n。从第 1 个人开始报数报到 m 的人出列然后从下一个人重新报数直到所有人出列。要求输出出列顺序。用队列模拟的思路非常直接先把 1 到 n 依次入队。然后循环执行从队头取出一个人如果当前报数不是 m就把他重新入队如果是 m就输出并彻底出队。每处理一个人报数计数器加一报到 m 后重置为 1。这个过程用循环队列或链式队列都可以。循环队列的好处是不需要频繁分配内存但需要预先知道 n 的大小。链式队列更灵活但每次重新入队都要new节点n 很大时性能差。实际测试下来n100000、m3 时循环队列比链式队列快 5 倍以上因为链式队列的new/delete开销太大了。4.2 约瑟夫环的 C 实现#include iostream #include queue void josephus(int n, int m) { std::queueint q; for (int i 1; i n; i) { q.push(i); } int count 1; while (!q.empty()) { int person q.front(); q.pop(); if (count m) { std::cout person ; count 1; } else { q.push(person); count; } } std::cout std::endl; }这段代码用std::queue实现逻辑清晰。但有一个细节count从 1 开始每次取出一个人后判断是否等于 m。如果等于 m输出并重置count为 1否则重新入队并count。注意重新入队时count已经加过了所以下一个人报的是count1。这个顺序不能搞反否则报数会错位。用std::queue的底层是std::deque性能比手写循环队列差一些但代码简洁适合面试手写。如果追求性能可以用循环数组实现void josephus_fast(int n, int m) { std::vectorint q(n); for (int i 0; i n; i) q[i] i 1; int front 0, rear n, count 1; int remaining n; while (remaining 0) { int person q[front]; front (front 1) % n; if (count m) { std::cout person ; count 1; --remaining; } else { q[rear] person; rear (rear 1) % n; count; } } std::cout std::endl; }这里用vector模拟循环队列front和rear在[0, n)范围内循环。注意rear初始为 n因为前 n 个位置已经被初始数据占满了。每次重新入队时把元素写到rear位置然后rear后移。因为出队和入队操作交替进行rear永远不会追上front所以不需要判满。这个实现比std::queue快很多因为没有动态内存分配。4.3 数学解法与队列解法的对比约瑟夫环有一个著名的数学递推公式f(n, m) (f(n-1, m) m) % n其中f(1, m) 0。这个公式可以在 O(n) 时间内算出最后幸存者的编号但只能求最后一个人不能输出完整的出列顺序。如果你只需要最后幸存者数学解法是最快的n1亿也能秒出结果。但如果要输出所有人出列的顺序就必须用模拟时间复杂度 O(n*m)因为每个人可能被重新入队多次。队列模拟的时间复杂度分析每轮出队一个人如果没报到 m 就重新入队相当于这个人被处理了 m 次最坏情况。总共有 n 个人所以总操作次数是 O(n*m)。当 m 很大时这个算法会很慢。优化思路是用数学方法直接计算出下一个出列的人的位置跳过中间的重新入队操作。比如当前队列有 k 个人从当前位置开始报数报到 m 的人出列那么出列位置就是(current m - 1) % k。这样每次出列只需要 O(1) 时间总时间复杂度降到 O(n)。但用数组实现时删除元素需要移动数据可以用链表或者标记数组来避免移动。注意约瑟夫环的变种很多比如从第 k 个人开始报数、报数方向交替、每次报数上限不同等。队列模拟的框架不变只需要调整初始入队顺序和报数逻辑。面试时如果遇到变种先问清楚规则再动手写。5. 常见问题与排查技巧实录5.1 循环队列的假溢出与边界 bug循环队列最容易出错的地方是判空和判满。如果用front rear判空那判满就不能也用这个条件。我见过有人写(rear 1) % capacity front判满但忘了队列实际容量是capacity - 1结果入队到最后一个位置时判断失误要么丢数据要么越界。排查方法在enqueue和dequeue里加断言检查count和front/rear的关系。比如enqueue后count应该加 1dequeue后count应该减 1。如果count和实际元素个数对不上说明状态管理有问题。另外用一个小容量比如 3手动模拟入队出队序列把每一步的front、rear、count打印出来很快就能定位。还有一个隐蔽的 bugfront和rear的初始值。如果初始化为 0第一个元素入队后rear变成 1front还是 0这没问题。但如果初始化为 -1取模运算(-1 1) % capacity在 C 里结果是 0看起来也对。但(-1) % capacity在 C 里是 -1不是 capacity-1因为 C 的取模对负数向零取整。所以如果front初始为 -1出队时front (front 1) % capacity第一次变成 0没问题但后续如果front变成 -1 就会出错。建议统一用 0 作为初始值避免负数取模的坑。5.2 链式队列的野指针与内存泄漏链式队列的dequeue操作里如果出队后队列为空必须把rear置空。这个 bug 的表现是出队最后一个元素后再入队一个新元素程序崩溃或者行为异常。原因是rear还指向已经被delete的节点rear-next node访问了已释放的内存。排查方法在dequeue里加日志打印front、rear和count。如果count变成 0 但rear不是nullptr就是这个问题。修复很简单在dequeue里加一行if (!front) rear nullptr;。内存泄漏的排查可以用 Valgrind 或者 AddressSanitizer。编译时加-fsanitizeaddress运行后如果有泄漏会直接报出泄漏的调用栈。我习惯在开发阶段一直开着 ASan虽然性能会降一些但能提前发现很多内存问题。5.3 约瑟夫环的报数错位与性能瓶颈约瑟夫环最常见的错误是报数错位。比如count初始化为 0 还是 1count m的判断是在出队前还是出队后重新入队时count有没有加。这些细节差一个结果就全错。我的建议是先用小数据手动验证。n5m3正确出列顺序是 3, 1, 5, 4, 2从 1 开始报数。写完后用这个用例跑一遍如果不对打印每一步的队列状态和count值很快就能找到问题。性能瓶颈方面如果 n 很大比如 100 万m 也很大比如 1000队列模拟会非常慢。这时候可以考虑用数学方法优化或者用链表加步进计算。但面试中一般 n 不会超过 1000队列模拟足够。如果面试官追问优化可以提数学递推和链表步进两种思路。5.4 常见问题速查表问题现象可能原因排查方法解决方案循环队列入队失败但队列不满判满条件错误打印front、rear、count用count判满或牺牲一个单元循环队列出队后数据错乱front或rear未取模检查取模运算是否遗漏所有移动都加% capacity链式队列出队最后一个元素后崩溃rear未置空检查dequeue后front是否为nullptr加if (!front) rear nullptr;链式队列内存持续增长节点未释放用 Valgrind 或 ASan 检测确保每个new都有对应的delete约瑟夫环出列顺序错误报数计数器逻辑错误用小数据手动验证明确count初始值和判断时机约瑟夫环 n 很大时超时队列模拟 O(n*m)分析时间复杂度用数学递推或链表步进优化提示调试队列问题时最有效的方法是把队列状态可视化。对于循环队列画一个环形图标出front和rear的位置对于链式队列画出节点和指针。纸上画一遍比盯着代码看半小时都管用。6. 从队列到环形缓冲实际项目中的扩展队列的思想在实际项目中远不止教科书上的入队出队。比如音频处理里的环形缓冲本质上就是一个循环队列但生产者和消费者是异步的需要处理读写指针的竞争。视频解码里的帧队列通常用链式队列加锁实现因为帧的大小不固定。任务调度器里的就绪队列可能是优先级队列但底层还是队列的变体。约瑟夫环的环形逻辑在分布式系统里也有影子比如一致性哈希的环、令牌桶的循环发放。理解队列的环形回绕和状态管理是理解这些复杂结构的基础。如果你想把这三个结构真正吃透我的建议是先手写一遍循环队列不加count变量只用front和rear判空判满感受一下边界条件的处理。然后写链式队列故意制造一次野指针崩溃再用调试器定位。最后用队列模拟约瑟夫环把 n 和 m 调大观察性能变化。踩过这些坑之后队列对你来说就不再是课本上的几行伪代码了。我在实际项目中用循环队列做过一个日志缓冲容量 8192生产者是多个线程消费者是单个写盘线程。一开始没加锁日志偶尔丢失后来用原子操作加内存屏障解决了。但那是另一个话题了队列本身的设计才是根基。把今天这三个结构写熟后面遇到再复杂的变体你都能一眼看穿它的本质。
延伸阅读

更多相关文章

2026/10/20 21:08:09

iOS图片选取避坑指南:UIImagePickerController实战与迁移

1. 为什么UIImagePickerController至今仍是绕不开的坎做iOS开发的人,几乎都经历过这样一个阶段:产品经理跑过来跟你说“加个选照片的功能,要能拍照、能选相册、能裁剪”,你心想这不就是调个系统API的事,半小时搞定。结…

2026/10/20 21:08:09

纯前端实现视频首帧截取:canvas与video配合的完整方案与避坑指南

1. 从一个真实需求说起:为什么视频封面不能靠手动截图做过内容管理后台或者视频上传功能的人,大概率都遇到过这个场景:用户上传一个视频文件,列表页需要展示一张封面图。最原始的做法是让用户自己截好图再上传,但实际用…

2026/10/20 21:08:09

OpenJDK 11.0.10 ARM64 Linux 部署指南:架构匹配、环境配置与生产验证

简介:本资源是面向ARM64架构Linux服务器与嵌入式开发者的OpenJDK 11.0.10官方构建版,专为aarch64平台深度适配,解决Java开发者在国产化ARM服务器、云原生边缘计算及信创环境下的JDK运行与开发需求。压缩包共508个文件,涵盖72个jmo…

2026/10/20 22:19:39

无人机三维航迹规划中改进动态多种群粒子群算法实战

做无人机三维路径规划,最让人头疼的不是地形建模,而是算法一收敛就再也跳不出来。我第一版用的是标准粒子群算法,20个粒子跑300代,结果每次都在同一条绕远、贴着威胁区飞过的路径上反复收敛,看起来算法很努力&#xff…

2026/10/20 22:19:39

PCIe中断机制深度解析:INTx、MSI与MSI-X的底层区别及驱动选型

做了这么多年 PCIe 驱动调试,我越来越觉得中断机制是被低估的一环。很多同学觉得 INTx、MSI、MSI-X 不过是三种叫法,选一个能用的就行。可真出了性能问题、中断风暴、共享冲突,回头一查才发现,这三兄弟从原理到使用差别巨大。这篇…

2026/10/20 22:19:39

MySQL数据误删恢复全攻略:binlog闪回、备份恢复与预防实战

凌晨两点,一条不带WHERE条件的DELETE语句执行完,屏幕上跳出一行"Query OK",下一秒你才反应过来——数据没了。这种场面在MySQL运维里太常见了。误删数据不可怕,可怕的是不知道有哪些恢复方案、每种方案各自能用在哪一步…

2026/10/20 22:19:39

AI编程的终点是SDD吗?从代码搬运工到意图架构师

说实话,我入行前几年就是个典型的代码搬运工。遇到问题第一反应是去各个技术社区和代码仓库搜现成答案,复制下来改吧改吧塞进项目里,能跑就算成功。这种状态持续很久,直到AI编程助手大规模普及,我才发现自己连"搬…

2026/10/20 22:19:39

MySQL时区问题排查:从8小时偏移到default-time-zone与连接串设置

你有没有过这种经历:某天凌晨被值班电话叫醒,说订单报表时间对不上,查了半天发现数据库时间和业务实际时间差着8个小时。我遇到过不止一次,而且每次表现还不一样——有时是全部时间都偏,有时是只有TIMESTAMP字段在偏&a…

2026/10/20 22:14:37

DeepSeek Harness桌面端:本地化提示词工作流与API密钥安全实践

“DeepSeek Harness 桌面端来啦”这句话,看着像是一个例行发版通知,真正换过去用之后才会发现,桌面端解决的不是“少开一个网页”的问题,而是把散落在网页对话框、脚本片段、备忘录里的模型调用资产,统一收回到本地。我…

2026/10/19 18:06:35

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/20 1:05:37

单片机C运行时从启动到多任务:中断安全与共享资源冲突

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

2026/10/20 1:05:37

GCC编译四步详解:预处理、编译、汇编、链接实战

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

2026/10/20 1:05:37

FIW线是什么?FEP注塑线与普通氟塑料线的区别及应用指南

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

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

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

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