力扣142.环形链表 II

力扣142.环形链表 II

这道题是面试高频考点,也是 LeetCode Hot100 中的经典题目,我们先讲简单的哈希表解法,再重点分析空间复杂度 O (1) 的快慢指针最优解。

一、简单解法:哈希表(Set 容器)

核心思路是利用哈希表的 “唯一性” 记录遍历过的节点:

  • 遍历链表时,将每个节点的地址插入 C++ STL 的set<ListNode*>容器;
  • 若当前节点已存在于set中,说明该节点就是环的第一个节点;
  • 若遍历到链表末尾仍无重复节点,则链表无环。

该方法逻辑简单易懂,此处不再展开赘述,接下来重点讲解更优的快慢指针解法。

二、最优解法:快慢指针(空间复杂度 O (1))
1. 第一步:判断链表是否有环

利用 “一快一慢” 两个指针遍历链表,通过是否相遇判断是否存在环:

  • 快指针(fast):每次走 2 步;
  • 慢指针(low):每次走 1 步;
  • 若链表无环:fast 指针会先走到链表末尾(fast == nullptrfast->next == nullptr),两个指针永远不会相遇;
  • 若链表有环:fast 指针会在环内 “追上” low 指针,最终两者必定相遇(因为 fast 每次比 low 多走 1 步,相当于每次向 low 靠近 1 步)。

(无环)

(有环)

2. 第二步:找到环的第一个节点

当确认链表有环后,我们需要进一步定位环的入口,先定义几个关键长度:

  • a:链表头结点到环入口的距离;
  • b:环入口到快慢指针相遇点的距离;
  • c:相遇点回到环入口的距离。

推导:

  • 慢指针(low)走到相遇点时,总路程为:a + b
  • 快指针(fast)走到相遇点时,总路程为:a + b + n×(b + c)n为 fast 绕环的圈数);
  • 由于 fast 速度是 low 的 2 倍,因此路程也为 2 倍:2×(a + b) = a + b + n×(b + c)
  • 化简公式得:a + b = n×(b + c)
为什么 n=1?(fast 必在 low 入环第一圈追上)

很多同学会疑惑 fast 是否会绕多圈才追上 low,答案是不会

  • 当 low 刚进入环时,fast 已经在环内,且两者的距离小于等于环的总长度 L;
  • fast 每次比 low 多走 1 步,相当于 以 1 步 / 次的速度追上 low;
  • 最多需要 L 步就能追上,而 low 走 L 步刚好是环的一圈,因此 low 入环后第一圈内必被 fast 追上。

因此n=1,代入公式继续化简:a + b = b + ca = c(头结点到环入口的距离 = 相遇点到环入口的距离)。

3. 第三步:定位环的第一个节点

基于a = c的核心结论,定位环入口:

  1. 当快慢指针相遇后,将 fast 指针重新移回链表头结点;
  2. 让 fast 和 low 指针以相同速度(每次走 1 步) 同时前进;
  3. 当两者再次相遇时,所处的位置就是环的第一个节点。
算法复杂度
  • 时间复杂度:O (n)
  • 空间复杂度:O (1)
这道题算法题主要考察了两个知识点
  1. 快慢指针相遇 → 链表有环;
  2. 相遇后 fast 回头结点,两指针同速前进 → 相遇点即为环入口。
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode *detectCycle(ListNode *head) { if(head==NULL||head->next==NULL) return NULL; ListNode*fast =head; ListNode*low =head; while(fast!=NULL&&fast->next!=NULL) { fast=fast->next->next; low=low->next; if(fast==low) break;//fast和low相遇 } if(fast!=low) return NULL; fast=head; while(fast!=low) { fast=fast->next; low=low->next; } return fast; } };
Could not load content