链表“追及”问题终极指南:快慢指针三部曲
快慢指针算法三部曲:从入门到精通
第一部曲:热身 - 寻找链表中点(★★★★☆)
核心思想:快指针速度是慢指针的 2 倍,当快指针到达终点时,慢指针恰好位于中点。
python
class Solution:def middleNode(self, head: ListNode) -> ListNode:slow, fast = head, headwhile fast and fast.next: # 快指针需能继续走两步slow = slow.next # 慢指针走1步fast = fast.next.next # 快指针走2步return slow
深度解析:
- 条件
fast and fast.next
的作用:fast is not None
确保快指针本身存在,避免空指针异常;fast.next is not None
确保快指针有下一个节点,可安全移动两步。
- 奇偶长度处理:
- 奇数长度(如
1→2→3
):快指针到3
后,fast.next=None
,循环停止,慢指针到2
(中点)。 - 偶数长度(如
1→2→3→4
):快指针到None
,循环停止,慢指针到3
(偏右中点)。
- 奇数长度(如
第二部曲:核心 - 判断链表是否有环(★★★★★)
核心思想:若链表有环,快指针终将在环内追上慢指针;若无环,快指针先到达终点。
python
class Solution:def hasCycle(self, head: ListNode) -> bool:slow, fast = head, headwhile fast and fast.next:slow = slow.nextfast = fast.next.nextif slow == fast: # 相遇即有环return Truereturn False
关键证明:
- 若无环,快指针先到终点,返回
False
; - 若有环,快指针比慢指针多跑一圈后必会相遇。例如:
环长为C
,快指针速度 2v,慢指针速度 v。进入环后,快指针相对慢指针的速度为 v,必然在C/v
时间内追上。
第三部曲:高潮 - 寻找环的起点(★★★★★)
算法步骤:
- 用快慢指针找到环内相遇点;
- 慢指针回表头,快慢指针同速移动,相遇点即为环起点。
python
class Solution:def detectCycle(self, head: ListNode) -> ListNode:slow = fast = head# 阶段1:找相遇点while fast and fast.next:slow, fast = slow.next, fast.next.nextif slow == fast:break# 无环判断if not fast or not fast.next:return None# 阶段2:找环起点slow = head # 慢指针回表头while slow != fast:slow, fast = slow.next, fast.nextreturn slow
数学推导:
设:
m
= 表头到环起点的距离;k
= 环起点到相遇点的距离;C
= 环的周长。
相遇时:- 慢指针路程:
m + k
; - 快指针路程:
m + k + nC
(多跑n
圈)。
因快指针速度是慢指针 2 倍:
plaintext
m + k + nC = 2(m + k) → nC = m + k → m = nC - k
结论:m
等于从相遇点绕环n
圈后后退k
的距离,故表头和相遇点出发的指针同速移动会在环起点相遇。
随堂测验解析
问题 1:快指针速度 3 步,慢指针 1 步,慢指针最终位置?
- 答案:链表长度的 1/3 处。
- 推导:设链表长
L
,快指针到终点时走了L
步(若L
是 3 的倍数),慢指针走了L/3
步。
问题 2:快慢指针首次相遇是否一定在环内?
- 答案:是。若没进环,快指针始终领先慢指针,无法相遇;只有进环后,快指针才能从后方追上慢指针。
问题 3:第二阶段用快指针从相遇点出发是否正确?
- 答案:正确。相遇时快慢指针在同一点,第二阶段慢指针回表头,快指针留在相遇点,同速移动必在环起点相遇。
你是否曾被链表问题搞得晕头转向?今天,我们将学习一个“降维打击”级别的技巧——。只需掌握这一招,你就能轻松破解一系列看似复杂的链表难题。快慢指针(又名“龟兔赛跑”算法)
我们将通过一个“三部曲”的形式,从热身到高潮,彻底征服它!
第一部曲:热身 - 寻找链表中点 (★★★★☆)
重要性评级:★★★★☆ (非常常见的面试题,是后续技巧的基础)
核心思想:快指针走两步,慢指针走一步。当快指针到达终点时,慢指针恰好在中间。
想象一下,在一条笔直的赛道上,兔子(快指针 'fas)的速度是(慢指针 'slo快慢)的两倍。当兔子跑到终点时,乌龟跑了多远?——正好是赛道的一半!
代码实现:
class Solution:def middleNode(self, head: ListNode) -> ListNode:# 初始化:乌龟和兔子都从起点 head 出发slow, fast = head, head# 比赛开始!只要兔子还能往前跑,就继续while fast and fast.next:# 乌龟走一步slow = slow.next# 兔子跑两步fast = fast.next.next# 比赛结束,乌龟所在的位置就是中点return slow
深度解析:为何如此精妙?同时 Fast 和 Fast。
这是这个算法最优雅的地方,它完美地处理了链表长度为奇数和偶数的两种情况:
-
奇数长度 (例如, 1->2->3->4->5):
-
快 的路径: 。1 -> 3 -> 5
-
当 指向快5 时,为fast.next (快速.下一个)没有,循环结束。
-
此时 指向 ,正是中点。慢3
-
-
偶数长度(例如,1->2->3->4):
-
快的路径: `1 -> 3 ->。1 -> 3 -> 无
-
当 'fa 指向 快3后,下一步 'fas会使快.next.next快变为 'N,循环结束。没有
-
此时 指向 慢3,是两个中点中的后一个,符合 LeetCode 题目要求。
-
你的观察非常敏锐!是为了处理偶数情况,fast 不是 Nonefast.next 不是 None是为了处理奇数情况。两者结合,代码无懈可击!
第二部曲:核心 - 判断链表是否有环 (★★★★★)
重要性评级: ★★★★★ (经典中的经典,快慢指针的代表作)
在一个环形赛道上,速度不同的两人赛跑,跑得快的人终将从后面“套圈”追上跑得慢的人。核心思想:
如果链表有环,那么它就像一个“圆形操场”。快慢指针进入环后,就如同在操场上赛跑。由于 比 '快慢快,必将在某一时刻追上快慢。如果 顺利到达了终点 (快没有),那就说明这是一条“直路”,没有环。
代码实现:
class Solution:def hasCycle(self, head: ListNode) -> bool: # -> bool 表示函数返回布尔值(True/False)slow, fast = head, headwhile fast and fast.next:slow = slow.nextfast = fast.next.next# 相遇了!说明赛道是环形的if slow == fast:return True# 兔子跑到了终点,说明没有环return False
这段代码几乎和找中点的一模一样,只是增加了一个“相遇”的判断。这就是优秀算法的特点:一个思想,多处应用。
第三部曲:高潮 - 寻找环的起点 (★★★★★)
重要性评级: ★★★★★ (的终极追问,深度考察你的理解)hasCycle 的
核心思想:这背后有一个绝妙的数学关系。
这是三部曲的高潮。如果链表有环,我们如何精确地找到环的入口?
算法分为两步:
-
第一阶段:和 `一样,用快慢指针找到环中的hasCycle 的相遇点。
-
第二阶段:将**一个指针放回头节点 `,另一个指针留在相遇点。然后,一个指针放回头节点头两个指针都以相同的速度(一次一步)前进。它们下一次相遇的地方,就是环的起点!
为什么会这样?——揭秘背后的数学之美
别担心,这个推导很简单:
-
设:
-
m = 从 到 环起点的距离头
-
k= 从环起点到相遇点的距离
-
C= 环的周长
-
-
当它们相遇时:
-
慢指针 走过的路程 =慢米 + 千
-
快指针 'fast走过的路程 = 'm + k + n快m + k + n*C (比 多跑了慢n圈)
-
-
因为 速度是快慢的两倍,所以 'dist_fast = 2 * dist_s:dist_fast = 2 * dist_slow
m + k + n*C = 2 * (m + k) -
化简得到:
n*C = m + k -
进一步变换:
m = n*C - k
这个公式 'm = n*C就是魔法的关键!它告诉我们:m = n*C - k
从 到环起点的距离 (头m),等于。这等价于,一个指针从从相遇点继续走圈再后退步的距离nk一个指针从出发,另一个指针从相遇点出发,都走步,它们会在环的起点相遇!头m
代码实现:
生成的 python
class Solution:def detectCycle(self, head: ListNode) -> ListNode:slow, fast = head, head# --- 第一阶段:找到相遇点 ---while fast and fast.next:slow = slow.nextfast = fast.next.nextif slow == fast:break # 相遇,跳出循环# 如果 fast 或 fast.next 是 None,说明 fast 走到了终点,没有环if not fast or not fast.next:return None# --- 第二阶段:寻找环的起点 ---# 将 slow 指针重新指向头节点slow = head# 两个指针以相同速度前进,直到再次相遇while slow != fast:slow = slow.nextfast = fast.next# 再次相遇点就是环的入口return slow
随堂测验 (检验你的掌握程度)
问题1: 在“寻找链表中点”的算法中,如果将快指针的速度改为每次走 3 步,慢指针仍然走 1 步。当快指针到达终点时,慢指针在什么位置?
答案: 慢指针会停在链表长度的 处。这个技巧可以推广,如果快指针速度是慢指针的 1/3k 倍,那么慢指针最终会停在 的位置。1/k
问题2: 在“判断链表是否有环”的算法中,慢指针 和快指针 慢快 第一次相遇一定是在环内吗?为什么?
<summary>点击查看答案</summary>
答案: 是的,一定在环内。因为只有进入了环, 才有机会从后面追上 快慢。如果还没到环的入口, 只会离 快慢 越来越远,不可能相遇。相遇本身就证明了环的存在,并且相遇点必然在环上。
问题3: 在“寻找环的起点”算法的第二阶段,我们让一个指针从 开始,另一个从相遇点开始,同速前进。如果我们将从相遇点开始的那个指针换成 头快(而不是在 后保持 break快 不动),结果还正确吗?
<summary>点击查看答案</summary>
答案: 完全正确。在 时,break慢 和 指向的是同一个节点(相遇点)。所以,在第二阶段的 快while 循环开始前, 被重置为 慢头,而 仍然停留在相遇点。代码 快while slow != fast 使用 作为从相遇点出发的指针是完全正确的,也是标准写法。快
恭喜你完成了快慢指针三部曲的学习!这个看似简单的技巧,背后蕴含着巧妙的逻辑和数学之美。掌握它,你将在链表的世界里游刃有余!
详细版:
如何求链表的中点呢?
一 我们可以先通过遍历获取链表的长度n,然后再次遍历一次得到n/2,从而获得中间节点。
二 一次遍历成功,我们通过增加指针的量数从而减少遍历的次数,
这次我们放两个指针一个快一个慢。
每当慢指针 slow
前进一步,快指针 fast
就前进两步,这样,当 fast
走到链表末尾时,slow
就指向了链表中点。
class Solution:def middleNode(self, head: ListNode) -> ListNode:# 快慢指针初始化指向 headslow, fast = head, head# 快指针走到末尾时停止while fast is not None and fast.next is not None: 你们可能会想这里只有fast.next is not one不可以吗?为什么要带上fast is not none呢? nononono 如果链表的长度是偶数的话,会有两个中点,第二次循环时fast指针就会指向none,因为判定条件如果只有fast.next is not none的话就会报错,因为fast本身在的位置就是none,下一个就不是none所以会报错,但如果加上fast is not None就会避免这种情况# 慢指针走一步,快指针走两步slow = slow.nextfast = fast.next.next# 慢指针指向中点return slow
如何判断链表中是否有环?
同样也是利用快指针以及慢指针,如果fast能够顺利的走到链尾就证明没有环,相反的如果最后fast和slow相遇了就代表链表包含环。
很简单是上一个题目的套版
class Solution:def hasCycle(self, head: ListNode) -> bool:其中->bool的意思是最后函数返回的结果是TRUE或者False# 快慢指针初始化指向 headslow, fast = head, head# 快指针走到末尾时停止while fast is not None and fast.next is not None:# 慢指针走一步,快指针走两步slow = slow.nextfast = fast.next.next# 快慢指针相遇,说明含有环if slow == fast:return True# 不包含环return False
既然判断了是否存在环,那么我们就将这个难度稍微的往上面加一加,如果存在环那么如何确定这个环的起点呢?
class Solution:def detectCycle(self, head: ListNode):fast, slow = head, headwhile fast and fast.next:fast = fast.next.nextslow = slow.nextif fast == slow:break# 上面的代码类似 hasCycle 函数if not fast or not fast.next: 这串代码的含义是如果not fast——如果fast是空,或者fast.next是空则说明不包含环,直接返回none。# fast 遇到空指针说明没有环return None# 重新指向头结点slow = head # 快慢指针同步前进,相交点就是环起点while slow != fast: 如果slow不等于fast就让其一直循环下面的代码直到slow等于fast,因为之前fast的速度是slow的两倍所以,这时slow被fast套圈了,slow运动N而fast运动2n(n为链表长度,两个指针相遇的地方就是链表的起点)fast = fast.nextslow = slow.nextreturn slow