【Hot100|24-LeetCode 141. 环形链表 - 完整解法详解】

一、问题理解

问题描述

给你一个链表的头节点 head,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true;否则,返回 false

示例

示例 1:

text

复制代码
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

(图:节点 -4 的 next 指向节点 2)

示例 2:

text

复制代码
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

text

复制代码
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

要求

  • 时间复杂度: O(n)

  • 空间复杂度: 哈希表解法 O(n),快慢指针解法 O(1)

  • 链表不可修改


二、核心思路:如何检测环?

基本思想

检测链表是否有环的核心问题是:在遍历过程中,如何判断一个节点是否已经被访问过?

有两种主流思路:

  1. 哈希表法(额外空间):记录每个访问过的节点,如果再次遇到,说明有环。

  2. 快慢指针法(Floyd 判圈算法):使用两个速度不同的指针遍历链表,如果存在环,快指针终将追上慢指针。

两种方法对比

方法 空间复杂度 优点 缺点
哈希表 O(n) 直观,容易理解 需要额外空间
快慢指针 O(1) 空间最优,符合进阶要求 需要理解"相遇"原理

三、代码逐行解析

方法一:哈希表法(使用额外空间)

Python 解法

python

复制代码
# Definition for singly-linked list.
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None

class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        # 使用集合存储已经访问过的节点
        seen = set()
        curr = head
        while curr:
            # 如果当前节点已经在集合中,说明有环
            if curr in seen:
                return True
            seen.add(curr)
            curr = curr.next
        # 遍历结束未重复,说明无环
        return False
Java 解法

java

复制代码
/**
 * 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) {
        // 使用哈希表存储访问过的节点
        Set<ListNode> seen = new HashSet<>();
        ListNode curr = head;
        while (curr != null) {
            // 如果当前节点已在集合中,返回true
            if (seen.contains(curr)) {
                return true;
            }
            seen.add(curr);
            curr = curr.next;
        }
        return false;
    }
}

方法二:快慢指针法(最优解,O(1) 空间)

核心原理
  • 定义两个指针:slow 每次走一步,fast 每次走两步。

  • 如果链表无环,fast 会先到达末尾(null)。

  • 如果链表有环,fast 最终会在环中追上 slow(相遇)。

Python 解法

python

复制代码
class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        # 初始化快慢指针
        slow = head
        fast = head
        
        # 快指针每次走两步,所以需要保证 fast 和 fast.next 不为空
        while fast and fast.next:
            slow = slow.next          # 慢指针走一步
            fast = fast.next.next     # 快指针走两步
            
            # 如果相遇,说明有环
            if slow == fast:
                return True
        
        # 循环结束说明 fast 到达末尾,无环
        return False
Java 解法

java

复制代码
public class Solution {
    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;
    }
}

四、Java 与 Python 语法对比

操作 Java Python
链表节点定义 class ListNode { int val; ListNode next; ListNode(int x) { val = x; } } class ListNode: def __init__(self, x): self.val = x; self.next = None
空值判断 node == null node is None
集合/哈希表 Set<ListNode> set = new HashSet<>(); set.add(node); set.contains(node); set_ = set() set_.add(node) node in set_
循环条件 while (fast != null && fast.next != null) while fast and fast.next:

五、实例演示

示例:head = [3,2,0,-4],环的入口为节点 2(索引 1)

链表结构:3 → 2 → 0 → -4 → 2(回到节点 2)

哈希表法过程
  1. 遍历节点 3,加入集合

  2. 遍历节点 2,加入集合

  3. 遍历节点 0,加入集合

  4. 遍历节点 -4,加入集合

  5. 再次访问节点 2,发现已在集合中 → 返回 true

快慢指针法过程
步骤 slow 位置 fast 位置 说明
初始 3 3 同时出发
1 2 0 slow 走一步到 2,fast 走两步到 0
2 0 2 slow 到 0,fast 到 2
3 -4 -4 slow 到 -4,fast 到 -4,相遇!→ 有环

六、关键细节解析

1. 为什么快慢指针一定会相遇?

假设链表有环,当 slow 进入环时,fast 已经在环内某处。此时相当于 fast 在追赶 slow,由于速度差为 1(每步相对靠近 1),fast 必然在有限步数内追上 slow

2. 快慢指针的初始化为什么可以相同?

初始化都指向 head,然后立即开始移动。即使链表有环,第一次移动后它们就会分开。也可以初始化 slow = head, fast = head.next,但需要额外处理边界,不如统一初始化简单。

3. 循环条件为什么是 while fast and fast.next

  • fast 检查:防止链表为空或 fast 走到末尾(无环情况)。

  • fast.next 检查:因为 fast 一次走两步,必须确保下一步存在,否则会引发空指针异常。

4. 如何处理空链表或单节点链表?

  • 空链表:head == null,直接返回 false

  • 单节点且无环:head.next == null,快慢指针循环条件不满足,返回 false

  • 单节点自环:此时 head.next = head,快慢指针会相遇吗?初始 slow = head, fast = head,循环中 slow = head, fast = head.next = headslow == fasttrue,返回 true,正确。

5. 哈希表法中为什么用集合而不是列表?

集合的查找时间复杂度为 O(1),而列表的 in 操作是 O(n),会使得整体复杂度变为 O(n²),不符合要求。


七、复杂度分析

方法一:哈希表法

  • 时间复杂度: O(n),每个节点最多访问一次,集合查找 O(1)。

  • 空间复杂度: O(n),最坏情况下存储所有节点。

方法二:快慢指针法

  • 时间复杂度: O(n)

    • 无环:fast 遍历完链表,约 n/2 步 → O(n)

    • 有环:相遇时慢指针走的步数不超过环长度 + 非环部分长度,总体 O(n)

  • 空间复杂度: O(1),只使用了两个指针。


八、其他解法

解法三:标记节点法(修改链表,不推荐)

遍历链表,将每个节点的 next 指向一个特殊标记(如自身),如果遇到标记过的节点,说明有环。但这种方法会破坏原链表结构,不符合题目"链表不可修改"的隐含要求。

python

复制代码
def hasCycle(self, head):
    dummy = ListNode(0)  # 标记节点
    curr = head
    while curr:
        if curr.next == dummy:  # 遇到过标记
            return True
        next_node = curr.next
        curr.next = dummy  # 标记当前节点
        curr = next_node
    return False

解法四:反转指针法(不推荐)

反转链表的过程中,如果遇到已经反转过的节点(即回到了之前的节点),说明有环。但反转会破坏链表,同样不可取。


九、常见问题与解答

Q1: 为什么快慢指针的步长必须一个是1,一个是2?可以是其他步长吗?

A1: 可以,但必须保证速度差为1(相对速度),才能确保在有限步内相遇。例如 slow 走1,fast 走3,相对速度为2,理论上也能相遇,但需要更精确的数学条件,且可能错过某些环(如环长度为奇数时可能永远追不上)。最常见的设定是1和2,简单且保证相遇。

Q2: 如何找到环的入口?

A2: 这是该问题的进阶(LeetCode 142)。当快慢指针相遇后,将其中一个指针移回 head,然后两者以相同速度(每次一步)前进,再次相遇的点即为环入口。

Q3: 如果链表很长,快慢指针会有溢出风险吗?

A3: 不会,指针只存储地址,不涉及长度计算。

Q4: 哈希表法中,节点对象作为键是否可行?

A4: 可行。在 Python 中,对象是哈希的(只要类定义了 __hash____eq__),默认使用 id 作为哈希值,可以唯一标识节点。Java 中 HashSet 存储对象引用,也是基于内存地址,因此能正确判断是否重复访问。


十、相关题目

1. LeetCode 142. 环形链表 II

题目: 找到环的入口节点,并证明快慢指针相遇后,一个指针从头开始,另一个从相遇点开始,再次相遇即为入口。

python

复制代码
def detectCycle(self, head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            # 相遇,找入口
            slow = head
            while slow != fast:
                slow = slow.next
                fast = fast.next
            return slow
    return None

2. LeetCode 160. 相交链表

题目: 找到两个无环链表的相交节点。思路是双指针分别遍历两个链表,走到末尾后切换到另一个链表头,相遇点即为相交点。这类似于快慢指针的变种。

3. LeetCode 287. 寻找重复数

题目: 给定一个包含 n+1 个整数的数组,数字范围 1~n,至少有一个重复数。要求不修改数组且 O(1) 空间。该题可以转化为环形链表 II 的问题:将数组索引视为节点,值视为 next 指针。

4. LeetCode 202. 快乐数

题目: 判断一个数是否为快乐数(最终能否变为 1)。也可以使用快慢指针检测循环。


十一、总结

核心要点

  • 检测环的本质: 判断是否再次访问到已遍历过的节点。

  • 最优解法: 快慢指针(Floyd 判圈算法),时间复杂度 O(n),空间复杂度 O(1)。

  • 关键操作: 使用两个速度不同的指针,利用"相遇"条件判断环的存在。

算法步骤(快慢指针)

  1. 初始化 slow = fast = head

  2. 循环条件:fast != nullfast.next != null

  3. 移动:slow = slow.nextfast = fast.next.next

  4. 如果 slow == fast,返回 true(有环)。

  5. 循环结束,返回 false(无环)。

复杂度对比

解法 时间复杂度 空间复杂度 是否修改链表
哈希表 O(n) O(n)
快慢指针 O(n) O(1)
标记节点 O(n) O(1) 是(破坏链表)

扩展思考

快慢指针是链表问题中非常强大的工具,不仅能检测环,还能用于:

  • 找链表中点(LeetCode 876)

  • 找环入口(LeetCode 142)

  • 判断回文链表(配合反转)

掌握快慢指针,相当于掌握了链表中"追及问题"的核心。环的检测是其中最简单也是最重要的应用,务必深刻理解其原理和代码实现。

相关推荐
yxc_inspire1 小时前
2026年寒假牛客训练赛补题(六)
算法
哈库纳1 小时前
dbVisitor 6.7.0 解读:公元前日期处理的两种方案
后端·算法·架构
AC赳赳老秦1 小时前
边缘AI落地趋势:DeepSeek在工业边缘节点的部署与低功耗优化技巧
人工智能·python·算法·云原生·架构·pygame·deepseek
Polaris北1 小时前
第二十五天打卡
算法
甄心爱学习2 小时前
【单调栈】【哈希】
算法·哈希算法
蚂蚁数据AntData2 小时前
DB-GPT 0.7.5 版本更新:基于 Falcon 评测集的Text2SQL评测体系全面升级,支持LLM/Agent两种评测模式和多环境评测
大数据·人工智能·算法·ai·开源
gihigo19982 小时前
粒子群优化(PSO)改进算法在全局最优解搜索中的应用
算法
L_Aria2 小时前
3875. 【NOIP2014八校联考第4场第2试10.20】星球联盟(alliance)
数据结构·算法·图论
仰泳的熊猫2 小时前
题目 1473: 蓝桥杯基础练习VIP-芯片测试
数据结构·c++·算法·蓝桥杯