用std::list模拟循环链表最省事,因其高效删除且迭代器不因删除失效;删节点必须用erase()返回值更新迭代器并手动绕回,硬模拟大m会超时,应改用数学递推公式f(k)=(f(k-1)+m)%k。

用 std::list 模拟循环链表最省事
约瑟夫环本质是“删节点 + 绕圈”,std::list 支持高效删除、迭代器不因其他元素删除而失效,比手写指针循环链表快且不易崩。别硬写 Node* 和 next 指针——调试时迭代器越界比空指针更难抓。
常见错误现象:std::list::erase() 返回下一个有效迭代器,但很多人直接 ++it 导致跳过或崩溃;还有人用 for (auto it = lst.begin(); it != lst.end(); ++it) 边删边遍历,这在 erase() 后会让 it 失效。
- 删完必须用
erase()的返回值更新迭代器:it = lst.erase(it) - 如果当前
it是最后一个,erase()返回lst.end(),得手动绕回lst.begin() - 人数少(比如
n )时性能差异可忽略;n 超过 10⁵ 再考虑数学公式解法
std::list 初始化和报数逻辑怎么写才不绕晕
报数不是“每第 m 个删”,而是“从当前位置起数 m-1 步,删掉第 m 个”。起点是上一轮删掉位置的下一个——所以第一次要从头开始数 m-1 步,不是 m 步。
使用场景:练习 STL 迭代器操作、理解循环行为边界。别在面试现场临时推下标公式,容易漏特例(比如 m=1 或 n=1)。
立即学习“C++免费学习笔记(深入)”;
- 初始化:
std::list<int> lst; for (int i = 1; i </int> - 设
auto it = lst.begin(),然后执行for (int i = 1; i - 删掉当前
it:it = lst.erase(it);如果it == lst.end(),立刻it = lst.begin()
手写循环链表指针版最容易栽在哪几个坑
真要练指针,核心不是“连成环”,而是“删节点时怎么维护环不断、迭代器不飘”。90% 的崩溃出在 delete 后还访问 next,或者 head 指针没及时更新。
错误信息典型如:heap-use-after-free(ASan 报)、Segmentation fault (core dumped)、输出结果漏人或重复。
- 删节点前,必须先保存
to_delete->next,再改前驱的next,最后delete to_delete - 如果删的是
head,记得更新head = head->next;只剩一个节点时,删完要置head = nullptr - 不要用
while (cur != head)遍历——初始cur == head就进不了循环。改用do-while或先判空
当 m 很大时,为什么不能硬模拟
比如 n = 1000, m = 10⁹,硬数会循环 10¹² 次。这时候必须换思路:数学递推公式 f(1) = 0, f(k) = (f(k-1) + m) % k(下标从 0 起),最后答案加 1。
注意:公式只适用于每次删第 m 个(即步长固定),不适用于“第 1 个删第 1 个、第 2 个删第 2 个”这种变步长场景。
- 递推要从
k = 2开始算到n,别错写成for (int k = 1; k - 中间模运算必须每步都做,否则
f(k-1) + m可能溢出int - 如果题目要求输出整个淘汰顺序,那只能模拟——公式只给最后幸存者
实际写的时候,先看数据范围。n 小就用 std::list;n 大 m 也大,优先套公式;只有老师明确说“必须用循环链表指针实现”,才动手连 next。指针版里最常忘的是删 head 后更新 head,一忘就整个链断成两截。











