一、核心思路
判断单链表是否存在环。链表有环:某个节点的 next 指针可以再次访问到之前出现过的节点;有环返回 true,无环返回 false。 采用弗洛伊德龟兔赛跑算法(快慢指针)
- 慢指针
slow每次走 1 步,快指针fast每次走 2 步,两个指针都从 head 出发。 - 如果链表不存在环:快指针会先走到链表末尾
null,退出循环,返回 false。 - 如果链表存在环:快指针进入环后会一直在环内循环,速度更快,最终一定会追上慢指针。当
slow == fast(节点引用相等,内存地址相同)代表相遇,证明存在环,返回 true。
循环条件:
fast != null && fast.next != null目的:保证执行fast.next.next不会触发空指针异常。
二、代码实现
/**
* Definition for singly‑linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
// 判断节点引用是否为同一个对象
if(slow == fast){
return true;
}
}
// fast走到null,链表无环
return false;
}
}
时间复杂度:(O(n)) 空间复杂度:(O(1)),仅使用两个指针变量
备选思路 HashSet:遍历把节点存入集合,检测重复节点,空间复杂度 O (n),不推荐面试使用。
三、实例演示
样例 1:有环链表 3 →2 →0 →‑4,‑4.next = 2
初始化:slow=3,fast=3,循环条件fast != null && fast.next != null
| 轮次 | slow | fast | slow = slow.next | fast = fast.next.next | slow == fast |
|---|---|---|---|---|---|
| 初始 | 3 | 3 | - | - | ❌ |
| 第 1 轮 | 3 | 3 | slow 走到 2 | fast 走到 0 | ❌ |
| 第 2 轮 | 2 | 0 | slow 走到 0 | fast 走到 2 | ❌ |
| 第 3 轮 | 0 | 2 | slow 走到‑4 | fast 走到‑4 | ✅相遇,return true |
样例 2:无环链表 1 →2 →3 →null
初始化:slow=1,fast=1
| 轮次 | slow | fast | slow = slow.next | fast = fast.next.next | slow == fast |
|---|---|---|---|---|---|
| 初始 | 1 | 1 | - | - | ❌ |
| 第 1 轮 | 1 | 1 | slow 走到 2 | fast 走到 3 | ❌ |
| 第 2 轮 | 2 | 3 | slow 走到 3 | fast.next == null,循环终止 | - |
循环退出,返回 false。
四、语法坑、边界问题
-
slow == fast比较的是节点对象引用(内存地址),不能比较 val 值。不同节点 val 相同,但不是同一个节点,不能判定有环。 -
while 循环条件
fast != null && fast.next != null,两个条件缺一不可;如果缺少fast.next != null,执行fast.next.next报空指针。 -
slow 与 fast 初始值都赋值
head,不要初始化为head.next,会错过初始就相遇的场景。 -
边界用例:
head == null空链表:直接跳出循环返回 false,代码兼容。- 单个节点,节点 next 指向自己(自环):快慢指针可以正常相遇返回 true。
-
有环链表永远不会出现
fast = null,指针一直在环中循环。
五、内置函数 / 语法总结
fast = fast.next.next:链表一次性向后跳两步,使用前必须保证 fast.next 不为 null。- 链表引用相等
==:判断是否是同一个节点对象;不能拿==判断节点内部数值。 - while 链表遍历终止:无环链表靠 fast 碰到 null 结束循环。
六、个人感悟
- 快慢指针两大用途:①找链表中点(234 回文链表);②检测链表环(141 环形链表);两套代码循环条件完全一样。
- 核心逻辑:有环,快指针一定能追上慢指针;无环,快指针先抵达链表末尾。
- 注意区分:
==对比地址,.val对比数值,链表题高频踩坑点。 - 拓展:142 环形链表 II,在本题基础上进一步寻找环的入口节点。