LeetCode 141. 环形链表|从 HashSet 到快慢指针 O (1) 空间最优解

一、题目简介

给你一个链表的头节点 head,判断链表中是否有环。 如果链表中某个节点,可以通过连续跟踪 next 指针再次到达,则说明链表中存在环。

  • 存在环 → 返回 true
  • 不存在环 → 返回 false

注意:题目中的 pos 是评测系统内部用来标识环的位置,不会作为参数传入,我们只需要根据 head 判断即可。


二、直观解法:HashSet

1. 思路

这应该是所有人第一反应都能想到的解法: 遍历链表,每访问一个节点,就把它存到集合里。

  • 如果当前节点已经在集合中:说明绕回来了,存在环
  • 如果走到 null 都没重复:说明没有环

2. 非常重要的细节

❌ 错误做法:存节点的 val 值 ✅ 正确做法:存节点对象本身

原因:链表中不同节点完全可能有相同的值。 比如 1 → 2 → 1 → 3 → null,值 1 出现了两次,但这是一条正常的无环链表。 只有同一个节点对象被第二次访问,才能说明存在环。

3. 完整代码

java 复制代码
import java.util.HashSet;
import java.util.Set;

public class Solution {
    public boolean hasCycle(ListNode head) {
        Set<ListNode> set = new HashSet<>();
        while (head != null) {
            // 当前节点已经访问过 → 有环
            if (set.contains(head)) {
                return true;
            }
            // 没访问过,加入集合
            set.add(head);
            head = head.next;
        }
        // 走到null都没重复 → 无环
        return false;
    }
}

4. 复杂度分析

  • 时间复杂度:O (n) 每个节点最多遍历一次,HashSet 的 add 和 contains 平均都是 O (1)。

  • 空间复杂度:O (n) 最坏情况下链表全是节点没有环,Set 要存下所有节点。


三、最优解法:快慢指针(Floyd 判圈算法)

1. 核心思想

想象一下操场跑圈:

  • 一个人慢跑(slow),一个人快跑(fast)
  • 如果跑道是直的(无环):跑得快的人会先跑到终点
  • 如果跑道是环形的(有环):跑得快的人迟早会从后面追上慢跑的人

对应到链表:

  • slow 每次走 1 步
  • fast 每次走 2 步
  • 如果有环:fast 一定会在环里追上 slow,两者相遇
  • 如果无环:fast 会先走到 null

2. 完整 AC 代码

java 复制代码
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 走到终点 → 无环
        return false;
    }
}

3. 逐行拆解

① 初始化
复制代码
ListNode slow = head;
ListNode fast = head;

快慢指针都从起点开始。

② 循环条件
复制代码
while (fast != null && fast.next != null)

这是新手最容易写错的边界。 因为 fast 一次走两步,所以必须保证:

  • fast != null:当前快指针不是空
  • fast.next != null:快指针的下一个也不是空 否则执行 fast.next.next 会空指针异常。
③ 移动指针
复制代码
slow = slow.next;
fast = fast.next.next;

慢一步,快两步。 注意:先移动,再判断相遇,初始都在 head,不能一开始就判断相等。

④ 相遇判断
复制代码
if (slow == fast) {
    return true;
}

这里比较的是节点引用(地址),不是 val。 两个指针指向同一个节点对象,说明 fast 追上了 slow,存在环。

4. 为什么有环就一定能相遇?

核心是相对速度。 假设已经进入环中:

  • slow 每次前进 1 步
  • fast 每次前进 2 步
  • 相当于 fast 相对于 slow,每次多走 1 步

假设两者在环中的距离是 k,那么每走一轮距离减少 1,最多 k 轮之后,fast 一定会追上 slow,两者相遇。

5. 为什么无环不会死循环?

如果链表没有环,fast 走得更快,一定会先到达链表末尾。 要么 fast == null,要么 fast.next == null,循环就会结束,返回 false。


四、新手必踩坑总结

  1. HashSet 存 val 不存节点 值重复不代表有环,必须存节点对象本身。

  2. 快慢指针初始就判断相等 初始都指向 head,肯定相等,必须先移动再判断。

  3. 循环条件只写 fast != null 漏掉 fast.next != null,走两步的时候会空指针。

  4. 比较 slow.val == fast.val 不同节点可以有相同的值,必须比较节点引用本身。


五、两种解法对比

表格

解法 核心思想 时间复杂度 空间复杂度 特点
HashSet 记录访问过的节点,重复则有环 O(n) O(n) 直观好理解,容易写
快慢指针 Floyd 判圈,快追慢 O(n) O(1) 空间最优,面试标准答法

六、面试口述标准回答

我首先想到的是用 HashSet 存储已经访问过的节点,遍历链表时如果遇到已经在集合中的节点,说明存在环。时间复杂度 O (n),空间复杂度 O (n)。

还可以进一步用快慢指针优化到 O (1) 空间。设置 slow 和 fast 两个指针,slow 每次走一步,fast 每次走两步。如果链表有环,fast 一定会在环内追上 slow;如果没有环,fast 会先到达链表末尾。时间复杂度仍然是 O (n),空间复杂度优化为常量级。


七、核心总结

这道题的核心可以浓缩成三句话:

  1. 直观思路用 Set,存节点不存值
  2. 最优解快慢指针,慢一步快两步
  3. 相遇即有环,快指针到终点即无环

HashSet 是保底思路,保证能写对;快慢指针是加分项,体现空间优化思维。面试时先讲直观思路,再讲优化解法,回答就非常完整了。

相关推荐
薛定e的猫咪23 分钟前
(NeurIPS 2022)GraphGPS:MPNN 与全局注意力的融合之道
人工智能·深度学习·学习·算法
AI服务老曹1 小时前
车牌识别算法接入AI视频分析平台的流程和误报优化
人工智能·算法·音视频
神威难绷泪1 小时前
Linux应用软件编程:线程分离属性 互斥机制 同步机制 死锁
linux·开发语言·算法·线程
小星星闪亮登场2 小时前
图论--最小生成树(内含二分图)
数据结构·算法·图论·迭代加深·图搜索算法
ZC跨境爬虫2 小时前
LeetCode 219. 存在重复元素 II(滑动窗口 + 哈希表详解)
算法·leetcode·散列表
吞下星星的少年·-·2 小时前
The 2025 ICPC Asia East Continent Online Contest (I) A题(模拟+贪心)
算法·贪心·模拟
小程序设计2 小时前
【机械设计】磁粉检测机器人的设计与验证
算法·机器人
Navigator_Z3 小时前
LeetCode //C - 1220. Count Vowels Permutation
c语言·算法·leetcode
吃好睡好便好3 小时前
判断函数的使用
学习·算法·matlab·生活·判断函数