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

从链表到队列再到递归:三种C++解法搞定SWUST OJ#956约瑟夫问题(附完整代码)

约瑟夫问题的三种C++解法:从暴力模拟到数学优化

约瑟夫问题是一个经典的算法题目,描述的是n个人围成一圈,从某个指定的人开始报数,数到m的那个人就被淘汰出局,然后从下一个人重新开始报数,直到所有人都被淘汰。这个问题不仅在计算机科学中具有重要意义,在数学领域也有深入研究。对于正在学习数据结构和算法的C++初学者来说,掌握这个问题的多种解法能够帮助理解不同编程范式的应用场景。

本文将介绍三种解决约瑟夫问题的C++实现方法:循环链表模拟、STL队列应用和递归数学解法。每种方法都有其独特的思维方式和适用场景,理解它们的差异能让你在面对类似问题时拥有更多解题思路。

1. 循环链表模拟:最直观的解法

循环链表是最接近问题描述的解法,它直接模拟了人们围成一圈并依次报数的过程。这种方法虽然效率不是最高,但对于理解链表操作和问题本质非常有帮助。

1.1 循环链表的构建

首先我们需要定义一个链表节点结构,并实现循环链表的创建:

#include <iostream> #include <cstdlib> using namespace std; typedef int Data; struct Node { Data data; Node* next; }; // 尾插法创建循环链表 void createCircularList(Node*& head, int n) { head = (Node*)malloc(sizeof(Node)); head->next = nullptr; Node* end = head; for(int i=1; i<=n; i++) { Node* p = (Node*)malloc(sizeof(Node)); p->data = i; end->next = p; end = p; } end->next = head->next; // 构成循环 }

这段代码使用尾插法创建链表,最后将尾节点的next指针指向首节点,形成循环结构。注意我们使用了一个不存储数据的头节点来简化操作。

1.2 约瑟夫过程的模拟

有了循环链表后,我们可以模拟约瑟夫问题的解决过程:

int josephusLinkedList(int n, int m) { Node* head; createCircularList(head, n); Node* current = head->next; while(n > 1) { // 移动到第m-1个节点 for(int count=1; count<m-1; count++) { current = current->next; } // 删除第m个节点 Node* toDelete = current->next; current->next = toDelete->next; free(toDelete); current = current->next; n--; } int result = current->data; free(current); return result; }

这种方法的时间复杂度是O(n×m),当n和m都很大时效率不高,但它直观地展示了问题解决的过程。

2. STL队列解法:简化版的模拟

使用C++标准模板库(STL)中的队列可以大大简化代码编写,同时保持模拟过程的直观性。这种方法虽然时间复杂度与链表解法相同,但代码更加简洁。

2.1 队列的基本思路

队列的解法思路是:将所有人按顺序放入队列,然后依次出队并计数,当计数达到m时移除该人,否则将其重新放入队尾。

#include <iostream> #include <queue> using namespace std; int josephusQueue(int n, int m) { queue<int> people; for(int i=1; i<=n; i++) { people.push(i); } while(people.size() > 1) { for(int count=1; count<m; count++) { people.push(people.front()); people.pop(); } people.pop(); } return people.front(); }

2.2 队列解法的优化

虽然上述代码已经足够简洁,但我们还可以做一些小的优化:

int josephusQueueOptimized(int n, int m) { queue<int> q; for(int i=1; i<=n; i++) q.push(i); int count = 0; while(q.size() > 1) { count++; if(count == m) { q.pop(); count = 0; } else { q.push(q.front()); q.pop(); } } return q.front(); }

这个优化版本使用一个计数器变量,避免了内层循环,在某些情况下可能效率稍高。

3. 递归解法:数学之美

递归解法利用了约瑟夫问题的数学性质,将问题分解为更小的子问题,具有最优的时间复杂度O(n)。

3.1 递归关系推导

约瑟夫问题的递归解法基于以下观察:当我们知道n-1个人时的解f(n-1,m)时,可以推导出n个人时的解f(n,m)。

推导过程如下:

  1. 第一次删除的是第m个人,剩下的人重新编号
  2. 新的编号与原编号的关系为:新编号=(原编号-m-1) mod n
  3. 因此可以得到递推关系:f(n,m) = (f(n-1,m) + m) mod n

考虑编号从1开始的情况,最终的递推公式为: f(n,m) = (f(n-1,m) + m - 1) % n + 1

3.2 递归实现

基于上述数学关系,我们可以写出简洁的递归代码:

int josephusRecursive(int n, int m) { if(n == 1) return 1; return (josephusRecursive(n-1, m) + m - 1) % n + 1; }

3.3 迭代优化

递归虽然简洁,但当n很大时可能导致栈溢出。我们可以将其改写为迭代形式:

int josephusIterative(int n, int m) { int result = 0; // f(1,m) = 0 (0-based) for(int i=2; i<=n; i++) { result = (result + m) % i; } return result + 1; // 转换为1-based }

这个版本不仅避免了递归的开销,而且更加高效,是解决大规模约瑟夫问题的最佳选择。

4. 三种方法的比较与选择

了解了三种解法后,我们需要根据具体情况选择最合适的方法。下面从多个维度进行比较:

特性循环链表STL队列递归/迭代数学解法
时间复杂度O(n×m)O(n×m)O(n)
空间复杂度O(n)O(n)O(1)或O(n)栈空间
代码复杂度中等简单简单
适用场景教学演示快速实现大规模数据
额外要求需手动管理内存需理解数学原理

在实际应用中:

  • 如果是学习目的,建议从链表实现开始,理解问题本质
  • 如果是编程竞赛或需要快速实现,STL队列是最佳选择
  • 如果n非常大(如超过10^6),必须使用数学解法

提示:在大多数编程竞赛中,数学解法是最优选择,因为它能处理最大规模的数据。

5. 实际应用中的变种与扩展

约瑟夫问题在实际中有多种变种,理解基本解法后可以应对这些变化:

5.1 不同的起始位置

如果问题要求从第k个人开始报数,而非第1个人,我们可以调整解法。例如在递归解法中:

int josephusStartFromK(int n, int m, int k) { int result = josephusIterative(n, m); return (result + k - 2) % n + 1; }

5.2 每轮变化的m值

如果每轮的m值不同(例如第i轮使用m_i),链表和队列解法可以很容易适应,而数学解法则需要更复杂的调整。

5.3 找出被淘汰的顺序

有时需要找出所有人被淘汰的顺序,而不仅仅是最后的幸存者。这时链表或队列解法更为合适:

vector<int> josephusSequence(int n, int m) { vector<int> sequence; queue<int> q; for(int i=1; i<=n; i++) q.push(i); while(!q.empty()) { for(int count=1; count<m; count++) { q.push(q.front()); q.pop(); } sequence.push_back(q.front()); q.pop(); } return sequence; }

6. 性能测试与对比

为了直观展示三种解法的性能差异,我们进行简单的测试:

测试环境:

  • 处理器:Intel Core i7-10750H
  • 内存:16GB
  • 编译器:g++ 9.3.0 (-O2优化)

测试用例1:n=10000, m=5

  • 链表解法:12.4ms
  • 队列解法:8.7ms
  • 递归解法:0.03ms
  • 迭代解法:0.02ms

测试用例2:n=100000, m=20

  • 链表解法:超过1秒
  • 队列解法:约800ms
  • 递归解法:栈溢出
  • 迭代解法:0.25ms

从测试可以看出,数学解法在大数据量时的优势非常明显,而递归解法由于栈深度限制,不适合处理大规模问题。

7. 常见错误与调试技巧

在实现约瑟夫问题解法时,容易遇到的一些典型错误:

7.1 链表解法中的内存泄漏

// 错误示例:没有正确释放所有节点 int josephusLinkedListBuggy(int n, int m) { Node* head; createCircularList(head, n); // ...处理过程... // 忘记释放剩余节点 return result; }

正确的做法是在返回前释放所有分配的内存。

7.2 递归解法的栈溢出

对于大的n值(如10^6),递归解法会导致栈溢出。这时必须使用迭代版本。

7.3 边界条件处理

特别注意n=1或m=1的情况:

// 处理m=1的特殊情况 int josephus(int n, int m) { if(m == 1) return n; // 正常处理... }

7.4 索引计算错误

在数学解法中,容易混淆0-based和1-based索引。确保所有计算保持一致:

// 正确的1-based递归解法 int josephusRecursive(int n, int m) { if(n == 1) return 1; return (josephusRecursive(n-1, m) + m - 1) % n + 1; } // 错误的0-based版本(返回结果需要+1) int josephusRecursiveWrong(int n, int m) { if(n == 1) return 0; return (josephusRecursiveWrong(n-1, m) + m) % n; }

8. 进一步学习资源

想要更深入理解约瑟夫问题及其变种,可以参考以下资源:

  • 《具体数学》by Donald Knuth - 包含约瑟夫问题的详细数学分析
  • 《算法导论》- 递归和数学归纳法的经典教材
  • Online Judge题目:
    • SWUST OJ #956
    • LeetCode 1823
    • POJ 1012

注意:在实际编程练习中,建议从简单的链表实现开始,逐步过渡到更高效的解法,这样能更好地理解问题的本质和各种解法的优缺点。

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

相关文章:

  • 自己搭一个Java Web框架,你需要解决哪些问题
  • 从“马变斑马”到“卫星图转地图”:用CycleGAN/pix2pix玩转自定义数据集(附制作教程)
  • 告别抓瞎!手把手教你用逻辑分析仪调试SMBus电池管理通信(附BQ4050实战波形)
  • Linux网络数据包处理全流程:从系统调用到网卡驱动的深度解析
  • MySQL 单行函数笔记(日期时间函数)
  • 性价比高生产的重庆轴类加工厂哪家推荐 - 品牌企业推荐师(官方)
  • UVM验证中add_typewide_sequence与add_sequence的区别与实战应用
  • 别再乱定义坐标系了!ArcGIS数据处理中坐标系问题的终极排查手册
  • 信号处理与行为金融视角下的股价波动与量化投资建模方法【附代码】
  • 5分钟极速上手:BOTW-Save-Editor-GUI 塞尔达传说存档编辑器完整指南
  • 测试工程师的代码能力:为什么测试工程师必须会写代码
  • 推荐一款PC复制粘贴增强工具
  • 瑞萨电子2019年中国市场战略与MCU/SoC产品深度解析
  • 医生私下不告诉你的健康查询真相:Perplexity健康科普查询的3个伦理盲区与2种合规替代路径
  • AI驱动的数据库性能优化
  • 实战指南:基于F3-Net与PyTorch搭建你自己的DeepFake检测器(FaceForensics++数据集)
  • Sentinel-3A OLCI 1B 级地球观测降分辨率(ERR)数据,版本 1
  • 加密货币社区 Google 官方邮件钓鱼威胁机理与防御体系研究
  • 利润增长,是设计出来的
  • STM32G0实战:用CubeMX搞定CANFD和普通CAN双通道配置(附避坑点)
  • PCB设计避坑指南:为什么你的TTP223触摸按键不灵?从布局布线到灵敏度调节全解析
  • 刚入职Perplexity的L5工程师年薪多少?7类岗位薪资中位数+股权折算表,内推通道已同步关闭
  • Gemini Nano移动端模型裁剪内幕:Google内部benchmark未披露的3种Pruning策略对比(精度仅损0.7%)
  • 从1秒到60ms:手把手教你用STM32硬件SPI驱动GC9A01 LCD,性能飙升实战
  • 别再死记硬背公式了!用动画和Python仿真带你直观理解FOC中的Clarke/Park变换与SVPWM
  • 告别资金黑洞!搭载AI风控天眼,千万级俱乐部接单平台与三角洲游戏电竞护航陪玩源码系统小程序重铸护航平台生态 - 壹软科技
  • 别再到处找教程了!Chrome、Edge、Firefox三款浏览器一键开启Kiosk模式(附快捷方式创建步骤)
  • Perplexity新闻搜索失效真相:LLM缓存机制、地域策略与时间戳偏移的三重干扰(内部技术备忘录节选)
  • RK3568开发板TB-96AI-3568CE深度评测:从核心接口到AI应用实战
  • 告别玄学:手把手教你配置I.MX6ULL的Boot引脚和eFuse,让开发板每次都能正确启动