Floyd算法是检测链表环的首选,因其空间复杂度为O(1)且无需哈希表;通过slow(步长1)和fast(步长2)指针相遇判断环存在,需严格检查fast与fast->next非空以避免段错误。

为什么 floyd 算法是检测链表循环的首选
因为它的空间复杂度是 O(1),不需要额外哈希表或标记位。核心思路是用两个指针以不同速度前进:一个每次走 1 步(slow),另一个每次走 2 步(fast)。如果存在环,fast 必然会在某次迭代中追上 slow。
注意:必须确保 fast 和 fast->next 都非空才继续推进,否则会触发空指针解引用——这是最常踩的段错误来源。
常见错误现象:Segmentation fault (core dumped),往往出现在没检查 fast->next == nullptr 就访问 fast->next->next。
如何安全实现 floyd 的循环检测逻辑
关键不是“能不能写出来”,而是边界条件是否全覆盖。典型结构如下:
立即学习“C++免费学习笔记(深入)”;
bool hasCycle(ListNode* head) {
if (!head || !head->next) return false;
ListNode* slow = head;
ListNode* fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) return true;
}
return false;
}使用场景:适用于单向链表、无额外存储权限、实时性要求不高的场合(如调试器检测、面试题)。
参数差异:输入必须是头指针 head;返回值仅为布尔型,不提供环入口位置——若需入口,得额外做一次同步推导。
检测到环后怎么找入口节点
找到相遇点只是第一步。真正难的是定位环的起始节点(即第一个被重复访问的节点)。这时要利用数学性质:设头到入口距离为 a,入口到相遇点距离为 b,环长为 c,则有 2(a + b) = a + b + n(c) → 推出 a = n(c) - b,意味着从头和从相遇点同时出发、同速前进,必在入口相遇。
实操建议:
- 先用
floyd确认有环,并保留slow或fast指针指向相遇点 - 新建指针
ptr1 = head,ptr2 = meet_node - 两者同步向前,直到
ptr1 == ptr2,该节点即为环入口
性能影响:找入口多一次遍历,时间复杂度仍为 O(n),但实际运行次数接近 2n;空间仍是 O(1)。
用 std::unordered_set 做替代方案行不行
可以,但不推荐用于高频或内存受限场景。它把每个节点地址插入集合,遇到重复地址即判定成环。
优点:逻辑直白,不易写错;能自然支持带数据比较的自定义链表(比如按值判重而非地址)。
缺点:
- 空间复杂度升至
O(n),可能触发大量内存分配 - 哈希冲突和桶扩容带来不可预测延迟
- 某些嵌入式或竞赛环境禁用 STL 容器
- 节点地址可能被复用(尤其短生命周期链表),导致误判
真正容易被忽略的是:std::unordered_set 存储的是指针值,不是对象内容;一旦链表节点被 delete 后又新分配在同一地址,就可能产生假阳性。


















