链表题解——环形链表 II【LeetCode】
142. 环形链表 II
快慢指针法
一、算法逻辑(每一步通顺讲解)
1️⃣ 初始化两个指针 slow 和 fast,均指向头节点
-
slow
每次走一步,fast
每次走两步; -
目标是通过“追及问题”判断链表是否存在环。
2️⃣ 快慢指针开始移动,进入 while 循环
-
如果
fast
变成None
或fast.next
是None
,说明链表无环(会提前退出); -
否则,继续让
slow
走一步、fast
走两步; -
一旦
slow == fast
,说明快慢指针在环中相遇。
3️⃣ 相遇后,引入新指针 ptr = head
,准备寻找环的入口
-
此时
slow
和fast
在环中某处; -
令
ptr
从头节点开始走,每次走一步; -
同时让
slow
继续走,每次也是一步; -
两者第一次相遇的地方,就是环的入口节点,返回它即可。
4️⃣ 如果整个过程中 fast
或 fast.next
是 None
,说明链表无环,返回 None
。
二、核心点
1. 快慢指针相遇代表链表有环;
2. 从相遇点和头节点分别出发,再次相遇点就是环的入口。
这个结论的数学推导是算法的核心,简要解释如下:
-
设链表头到环入口的长度为
a
,环的长度为b
; -
相遇时,slow 走了
a + x
步,fast 走了a + x + nb
(n是环内转的圈数); -
又因为 fast 是 slow 的两倍速度:
2(a + x) = a + x + nb → a = nb - x
-
所以从相遇点走
a
步,刚好能回到环入口; -
同理,从头节点走
a
步也刚好到环入口; -
因此:从
head
和slow
同时一步步走,第一次相遇的位置就是环的入口。
class Solution:def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:slow = fast = head # 初始化慢指针和快指针,都指向链表头节点while fast is not None: # 当快指针不为空时slow = slow.next # 慢指针每次移动一步if fast.next is None: # 如果快指针的下一个节点为空return None # 说明链表没有环,返回 Nonefast = fast.next.next # 快指针每次移动两步if fast == slow: # 如果快慢指针相遇ptr = head # 初始化一个新指针,指向链表头节点while ptr != slow: # 当新指针和慢指针不相遇时ptr = ptr.next # 新指针每次移动一步slow = slow.next # 慢指针每次移动一步return ptr # 返回新指针指向的节点(即环的入口节点)return None # 遍历结束,没有发现环,返回 None
上面思路和图解来自灵茶山艾府
三、时间复杂度分析
-
第一次相遇: 快慢指针最多走
O(n)
步(n 是链表长度); -
寻找入口: 再走一段最多也是
O(n)
; -
所以总时间复杂度为:
时间复杂度:O(n)
四、空间复杂度分析
-
使用了几个指针变量,但没有使用额外的数据结构;
-
所以空间复杂度为:
空间复杂度:O(1)
✅ 总结一句话
该算法通过快慢指针的相遇与数学性质,在线性时间和常数空间下找出链表环的入口节点,核心在于“环形追赶+数学推导”。