LeetCode 141:环形链表------用快慢指针判断链表是否成环
一、题目描述
今天做的是 LeetCode 141:环形链表。
题目要求判断一个单链表中是否存在环。

所谓环,就是链表中的某个节点的 next 指针重新指向了前面的某个节点。
例如正常链表:
text
1 → 2 → 3 → 4 → null
最终会走到 null,所以不存在环。
而如果:
text
1 → 2 → 3 → 4
↑ ↓
← ←
链表就会一直循环下去。
要求返回:
text
存在环 → true
不存在环 → false
二、我的思路:快慢指针
看到这道题,我想到的是链表中比较经典的:
快慢指针。
定义两个指针:
java
ListNode fast = head;
ListNode slow = head;
其中:
text
slow:每次走一步
fast:每次走两步
所以遍历的时候:
java
fast = fast.next.next;
slow = slow.next;
如果链表没有环:
text
slow
↓
1 → 2 → 3 → 4 → null
fast
↓
最终 fast 会走到 null。
而如果链表存在环,那么进入环以后:
text
slow → →
fast → → →
虽然 fast 一开始领先 slow,但是因为两个人都在一个环里面不断移动,而且 fast 每次比 slow 多走一步,所以最终一定会追上 slow。
因此判断逻辑就很简单:
text
fast 到 null
↓
没有环
fast == slow
↓
存在环
三、我的代码
最终代码:
java
public class Solution {
public boolean hasCycle(ListNode head) {
// 链表为空,或者只有一个节点
// 都不可能形成环
if(head == null || head.next == null){
return false;
}
ListNode fast = head;
ListNode slow = head;
while(fast != null && slow != null){
// fast 如果已经没有下一个节点,
// 说明链表到头了,不存在环
if(fast.next == null){
return false;
}
// fast 一次走两步
fast = fast.next.next;
// slow 一次走一步
slow = slow.next;
// 两个指针相遇,说明存在环
if(fast == slow){
return true;
}
}
return false;
}
}
四、为什么 fast 要检查 fast.next?
这里我觉得是代码中最需要注意的地方:
java
if(fast.next == null){
return false;
}
因为下面马上要执行:
java
fast = fast.next.next;
如果:
text
fast.next == null
那么继续访问:
java
fast.next.next
就会出现空指针问题。
所以在让 fast 一次走两步之前,至少需要保证:
text
fast.next 存在
也就是:
java
if(fast.next == null){
return false;
}
五、为什么 slow 不需要单独判断 slow.next?
代码中:
java
while(fast != null && slow != null)
其实这里 slow != null 的判断有点多余。
原因是:
fast的速度比slow快。
如果 slow 已经走到了 null,那么 fast 肯定也不可能还在正常链表中继续运行。
而真正可能出现的问题是:
java
fast.next == null
因为 fast 一次要走两步。
所以代码实际上可以稍微简化成:
java
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
return true;
}
}
return false;
这是我更推荐记住的写法。
六、为什么两个指针相遇就一定有环?
这个问题是这道题最核心的地方。
假设链表没有环:
text
1 → 2 → 3 → 4 → null
无论 fast 走得多快,最终都会走到 null。
所以:
text
没有环 → fast 到 null
但是如果存在环:
text
1 → 2 → 3 → 4
↑ ↓
← ← ←
当 slow 和 fast 都进入环以后,可以把它们理解成在一个跑道上跑步:
text
slow:每次走 1 格
fast:每次走 2 格
那么每一轮:
text
fast 比 slow 多走 1 格
假设环长度为 5,两个人之间相差 3 格。
那么:
text
第 1 次:相差 2
第 2 次:相差 1
第 3 次:相差 0
最终就会相遇。
所以:
只要两个指针都进入同一个环,并且快指针比慢指针每次多走一步,那么快指针最终一定能够追上慢指针。
这就是 Floyd 判圈算法的核心。
七、这道题其实不需要额外的数据结构
如果使用 HashSet,也可以判断链表是否有环。
思路是:
text
遍历节点
↓
把访问过的节点放进 Set
↓
如果某个节点已经存在
↓
说明出现重复
↓
存在环
但是这样需要保存访问过的节点:
text
O(n)
而快慢指针只需要:
java
ListNode fast;
ListNode slow;
所以额外空间复杂度是:
text
O(1)
时间复杂度:
text
O(n)
因此快慢指针更加适合这道题。
八、这次对快慢指针的理解
之前遇到链表题的时候,我更多把快慢指针理解成一种"固定套路":
text
一个走一步
一个走两步
但这道题让我更加理解了它为什么能够解决问题。
关键其实不是:
"快指针和慢指针速度不同。"
而是:
当它们进入环以后,速度差会不断缩小它们之间的距离。
在环中:
text
slow 每次 +1
fast 每次 +2
所以相对运动速度就是:
text
2 - 1 = 1
只要环存在,最终一定会相遇。
九、总结
这道题的代码并不长,但它是一个非常经典的链表问题。
我的最终思路可以概括成:
text
slow 每次走一步
fast 每次走两步
↓
如果 fast 到达 null
↓
没有环
如果 fast == slow
↓
存在环
最终复杂度:
text
时间复杂度:O(n)
空间复杂度:O(1)
这道题最大的收获就是理解了快慢指针为什么能够判断环,而不只是记住:
java
fast = fast.next.next;
slow = slow.next;
另外,自己的代码虽然可以通过,但:
java
while(fast != null && slow != null)
中的 slow != null 实际上可以省略。
更常见、也更简洁的写法是:
java
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
return true;
}
}
return false;
这次我也算是把一个新的链表技巧真正记下来了:
快慢指针不仅可以用来找链表中点,也可以用来判断链表是否存在环。