力扣解题-141. 环形链表
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
提示:
链表中节点的数目范围是 [0, 10⁴]
-10⁵ <= Node.val <= 10⁵
pos 为 -1 或者链表中的一个 有效索引 。
进阶:你能用 O(1)(即,常量)内存解决此问题吗?
Related Topics
哈希表、链表、双指针
第一次解答
解题思路
核心方法:哈希集合记录访问过的节点,通过HashSet存储遍历过程中访问过的链表节点,若遍历到重复节点则说明存在环,逻辑直观但需要O(n)的额外空间。
核心逻辑拆解
判断链表是否有环的核心是“是否会重复访问同一个节点”:
- 初始化:创建空的HashSet用于存储已访问的节点,定义
current指针从链表头节点head开始遍历; - 遍历链表:
- 若
current为null(遍历到链表尾部),说明无环,返回false; - 检查
current是否在HashSet中:- 存在:说明之前访问过该节点,链表有环,返回true;
- 不存在:将
current加入HashSet,current移动到下一个节点(current.next);
- 若
- 终止条件:要么找到重复节点(有环),要么遍历到null(无环)。
性能说明
- 时间复杂度:O(n)(每个节点最多被访问一次,HashSet的
contains和add操作均为O(1)); - 空间复杂度:O(n)(最坏情况链表无环,HashSet存储所有n个节点);
- 优势:
- 逻辑简单易懂,新手易上手;
- 遍历过程中发现环立即返回,无需遍历完整链表;
- 劣势:额外使用HashSet存储节点,空间复杂度未满足进阶的O(1)要求。
public boolean hasCycle(ListNode head) {
Set<ListNode> visited = new HashSet<>();
ListNode current = head;
while (current != null) {
// 如果当前节点已经访问过,说明有环
if (visited.contains(current)) {
return true;
}
// 记录当前节点
visited.add(current);
// 移动到下一个节点
current = current.next;
}
// 遍历到 null,说明无环
return false;
}
示例解答
解题思路
解法1:快慢指针法(弗洛伊德循环检测,最优解)
核心方法:双指针(慢指针+快指针),慢指针每次走一步,快指针每次走两步,若链表有环则快慢指针必会相遇;若无环则快指针会先遍历到null,时间复杂度O(n)、空间复杂度O(1),满足进阶要求。
核心原理铺垫
该方法的核心类比“操场跑步”:
- 无环链表:快指针(跑得快的人)会先到达终点(null);
- 有环链表:快慢指针进入环后,快指针会绕环追上慢指针(相遇);
- 数学证明:设环的长度为L,快慢指针进入环时的距离差为d,快指针每次比慢指针多走1步,最多L步后必然相遇。
核心逻辑拆解
- 边界处理:若头节点
head为null,直接返回false(空链表无环); - 初始化指针:慢指针
slow指向head,快指针fast指向head.next(避免初始状态快慢指针重合); - 循环检测:
- 若
slow != fast,进入循环:- 检查快指针是否到达终点(
fast == null或fast.next == null),若是则返回false; - 慢指针走一步(
slow = slow.next); - 快指针走两步(
fast = fast.next.next);
- 检查快指针是否到达终点(
- 若
slow == fast,说明相遇,返回true;
- 若
- 终止条件:要么快慢指针相遇(有环),要么快指针到null(无环)。
具体步骤(以示例1 head=[3,2,0,-4],pos=1为例)
- slow=3,fast=2;
- 第一次循环:slow=2,fast=0→-4→2;
- slow=2,fast=2,相遇,返回true。
性能说明
- 时间复杂度:O(n)(无环时快指针遍历n个节点,有环时最多遍历n+L个节点,L≤n);
- 空间复杂度:O(1)(仅使用两个指针变量,无额外存储);
- 核心优势:
- 满足进阶的O(1)空间要求,内存开销极小;
- 无需额外数据结构,执行效率更高;
- 相遇时立即返回,无需遍历完整链表。
public boolean hasCycle(ListNode head) {
if(head==null){
return false;
}
ListNode slow=head;
ListNode fast=head.next;
while (slow!=fast){
if(fast==null||fast.next==null){
return false;
}
slow=slow.next;
fast=fast.next.next;
}
return true;
}
解法2:快慢指针简化版(更通用的初始化方式)
核心方法:快慢指针的另一种初始化方式(均从head开始),逻辑更简洁,避免head.next为空导致的空指针异常。
代码实现
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
// 相遇则有环
if (slow == fast) {
return true;
}
}
// 快指针到终点,无环
return false;
}
优势说明
- 初始化更通用:快慢指针均从head开始,避免
head.next为null时的异常; - 逻辑更直观:循环条件直接检查快指针是否可达,相遇判断放在循环内,新手易理解;
- 性能与原快慢指针法一致,仅代码风格差异。
解法3:节点标记法(另类思路)
核心方法:遍历过程中修改节点值/指针标记访问状态,将访问过的节点val设为特殊值(如Integer.MAX_VALUE),若遍历到该特殊值则说明有环,空间复杂度O(1)但会修改原链表。
代码实现
public boolean hasCycle(ListNode head) {
ListNode current = head;
while (current != null) {
// 检测到标记,说明有环
if (current.val == Integer.MAX_VALUE) {
return true;
}
// 标记已访问的节点
current.val = Integer.MAX_VALUE;
current = current.next;
}
return false;
}
适用场景说明
- 空间复杂度:O(1),满足进阶要求;
- 局限性:
- 修改了原链表的节点值,破坏数据完整性;
- 若节点值本身包含
Integer.MAX_VALUE,会导致误判;
- 适用场景:仅作思路拓展,实际工程中禁止修改输入数据,因此不推荐使用。
总结
- 哈希集合法(第一次解答):O(n)时间+O(n)空间,逻辑直观但空间开销大,适合理解核心思路;
- 快慢指针法(最优解):O(n)时间+O(1)空间,满足进阶要求,无额外存储且执行高效,工程首选;
- 节点标记法:O(n)时间+O(1)空间,但修改原链表,仅作思路拓展;
- 关键技巧:
- 环形检测优先用快慢指针法,是链表/数组循环检测的经典贪心策略;
- 快慢指针初始化需注意:避免初始重合(如示例解法中fast=head.next),或在循环内先移动再判断;
- 边界处理:空链表、单节点链表需直接返回false,避免空指针异常。