循环链表解法需维护prev指针执行prev->next=curr->next删除,避免head被删或n=1时崩溃;k大于当前人数时用k%count处理,余0则删prev所指前驱;公式法f(n,k)=(f(n−1,k)+k)%n默认下标从0起,结果需+1转为1~n编号。

用循环链表模拟约瑟夫环时,节点删除的边界怎么处理?
链表解法直观但极易在 head 被删、只剩一个节点、或 step == 1 时崩溃。关键不是“遍历到第 k 个”,而是“在前一个节点上做删除”——否则会丢失前驱指针。
- 始终维护
prev指针,让prev->next = curr->next,再delete curr;不要尝试用curr自己删自己 - 当
n == 1,直接返回head->val,跳过所有循环逻辑 -
k可能远大于当前剩余人数,用k % count(注意:若余数为 0,等价于删倒数第一个,即prev当前指向的就是待删节点的前驱) - 构建循环链表后,务必检查
tail->next == head,否则遍历会提前终止
数学公式 f(n, k) = (f(n−1, k) + k) % n 怎么安全落地?
递推公式快且省空间,但初学者常错在索引偏移和 base case。它默认编号从 0 开始,而题目通常要求 1~n 编号,必须最后加 1。
- base case 是
f(1, k) = 0(不是 1),因为单人时“幸存者下标”只能是 0 - 递推中每轮的模运算是对当前人数
i取模,不是固定对n:写成res = (res + k) % i,其中i从 2 到n - 若输入
k == 1,结果就是n,可提前返回,避免无谓循环 - 注意整数溢出风险:当
n达百万级、k也很大时,res + k可能溢出int,建议用long long或先取模:(res + k % i) % i
两种解法性能与适用场景怎么选?
不是“哪个更好”,而是“哪个不拖垮你的运行环境”。小规模(n )两者无感;大规模时差异立刻显现。
- 链表法时间 O(nk),空间 O(n);当
k很大(如 1e6)、n中等(如 1e4)时,可能超时 - 公式法时间 O(n),空间 O(1),但只适用于“每次删第 k 个”的经典约瑟夫模型;若规则变为“奇数轮删、偶数轮跳过”,它就失效
- 调试阶段优先用链表法:你能
cout每轮删的是谁,验证逻辑;上线求稳且规则固定,切公式法 - LeetCode 390(消除游戏)看着像约瑟夫,但步长动态变化,此时链表模拟+双端队列优化更实际,公式不适用
为什么用 std::list 实现反而容易出错?
因为 std::list 的迭代器在 erase() 后立即失效,且不支持算术运算(it + k 非法)。强行用它模拟,代码会比手写链表还臃肿。
- 别写
auto it = lst.begin(); advance(it, k-1); lst.erase(it);——advance是 O(k),整体退化成 O(nk²) - 若坚持用容器,
std::vector配合下标移动更可控:删元素后用erase(v.begin() + idx),然后调整idx = (idx + k - 1) % v.size() - 手写单向循环链表 15 行内搞定,内存布局连续性差但逻辑干净;
std::list的双向特性和内存碎片反而干扰核心逻辑理解
实际写的时候,最常被忽略的是:公式法的 base case 错写成 f(1,k)=1,导致所有结果偏移 1;链表法在只剩两个节点时,prev 和 curr 指针关系突然反转,一不留神就删错人。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











