算法学习记录16——Floyd 判圈算法(环形链表 II)

最近在刷 LeetCode 的时候,遇到了一道经典题目:给定一个链表的头节点,返回链表开始入环的第一个节点;如果无环,就返回 null 。题目还特别强调:不能修改链表 。看题解了解到了 Floyd 判圈算法(快慢指针) ,感觉本质上还是数学

一开始我对这个算法只是有个模糊的印象------"快慢指针能判断有没有环",但具体怎么找到环的入口?为什么那样做是对的?这些细节我其实并不清楚。于是,我决定花点时间彻底搞懂它,并把整个思考过程记录下来。


🧩 问题理解

首先明确题意:

  • 链表可能有环,也可能没有。
  • 如果有环,我们要返回环的起始节点(即第一个被重复访问的节点)。
  • 不能修改链表结构(比如打标记、反转等都不行)。

所以,我们的目标是:只通过指针移动,在 O(1) 空间内找到环的入口


🔍 第一步:如何判断链表有环?

这一步我之前学过------用快慢指针(Floyd 判圈算法的第一阶段):

  • slow 每次走 1 步,fast 每次走 2 步。
  • 如果链表无环,fast 最终会走到 null
  • 如果有环,fast 一定会在某个时刻追上 slow(因为它们在环里跑,速度差为 1)。

这部分逻辑不难,代码也简单:

python 复制代码
slow = fast = head
while fast and fast.next:
    slow = slow.next
    fast = fast.next.next
    if slow == fast:
        # 有环!
        break
else:
    return None  # 无环

但问题来了:相遇点 ≠ 环的入口。那怎么从相遇点找到真正的入口呢?


🤔 第二步:为什么重置一个指针到 head 就能找到入口?

这是我最困惑的地方。网上很多教程直接说:"把 slow 放回 head,然后两个指针一起走,相遇就是入口。"

为什么?这背后一定有数学依据。

于是我画了个图,设了几个变量:

  • a:从链表头到环入口的距离。
  • b:从环入口到快慢指针第一次相遇点的距离。
  • c:从相遇点再回到环入口的距离(所以环总长 = b + c)。

slowfast 第一次相遇时:

  • slow 走了 a + b 步。
  • fast 走了 a + b + n(b + c) 步(n ≥ 1,因为它可能绕了多圈)。

又因为 fast 的速度是 slow 的两倍,所以路程也是两倍:

复制代码
2(a + b) = a + b + n(b + c)
=> a + b = n(b + c)
=> a = (n - 1)(b + c) + c

这个式子太关键了!

它说明:从 head 出发走 a 步,等于从相遇点出发走 c 步,再加上若干整圈

c 正好是从相遇点回到环入口的距离!

所以,如果我们:

  1. slow 重置到 head
  2. slowfast 都以每次 1 步的速度前进;

那么当 slow 走了 a 步到达入口时,fast 也刚好从相遇点走了 a 步------而根据上面的等式,这正好让它也到达入口!

于是,它们会在环的入口处相遇

💡 这一刻我恍然大悟:原来不是"碰巧",而是数学必然!


✅ 完整实现

结合以上分析,代码就清晰了:

python 复制代码
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None

def detectCycle(head: ListNode) -> ListNode:
    if not head or not head.next:
        return None

    slow = fast = head

    # 第一阶段:检测是否有环
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            break
    else:
        return None  # 无环

    # 第二阶段:找环的入口
    slow = head
    while slow != fast:
        slow = slow.next
        fast = fast.next

    return slow  # 或 fast,此时两者相等

⚠️ 实现中的注意事项

在写代码时,有几个容易踩的坑,我总结如下:

  1. 边界情况要处理

    • 空链表(head is None)或只有一个节点且无环的情况,直接返回 None
  2. 循环条件别写错

    • 必须是 while fast and fast.next,否则 fast.next.next 会报错。
  3. 不要提前重置指针

    • 只有在确认有环(即 slow == fast)之后,才能重置 slow = head
  4. 空间复杂度是 O(1)

    • 整个过程只用了两个指针,完全符合题目"不允许修改链表"和"常数空间"的要求。
  5. 时间复杂度是 O(n)

    • 第一阶段最多走 2n 步,第二阶段最多走 n 步,总体线性。

🧪 举个例子验证一下

假设链表是:3 → 2 → 0 → -4 → 2(环),即 pos = 1

  • a = 1(head 到节点 2)
  • 环是 2 → 0 → -4 → 2,长度为 3
  • 快慢指针可能在 -4 相遇(b = 2, c = 1

根据公式:a = (n-1)*3 + 1,当 n=1 时,a = 1,成立。

重置 slow 到 head(3),然后:

  • slow: 3 → 2
  • fast: -4 → 2

在节点 2 相遇,正是环的入口!✅


相关推荐
代码游侠6 小时前
学习笔记——进程控制函数
linux·运维·笔记·学习·算法
小O的算法实验室6 小时前
2022年CIE SCI2区TOP,双向交替搜索 A* 算法的移动机器人全局路径规划,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
木头左6 小时前
多任务联合训练框架下的遗忘门协同优化趋势跟踪与均值回归双目标平衡
算法·均值算法·回归
xu_yule6 小时前
算法基础-(单调队列)
算法·单调队列
冬夜戏雪6 小时前
【学习日记】【12.15】【13/60】
学习
QiZhang | UESTC6 小时前
学习日记day49
学习
代码不停6 小时前
Java递归综合练习
java·开发语言·算法·回归
前端小白在前进6 小时前
力扣刷题:删除排序链表的重复元素Ⅱ
算法·leetcode·链表
石像鬼₧魂石6 小时前
Fail2Ban 一键部署 + 管理脚本(可直接执行)
linux·windows·学习·ubuntu