刷题打卡到第 10 天,今天轮到的是 Leetcode 141:环形链表。这道题在链表类的题目里属于“看着简单,但真想把它讲清楚并不容易”的典型代表。题目本身只要求判断链表里有没有环,但无论你是刚接触链表的 JavaScript 新手,还是准备面试时想刷经典题的开发者,都值得停下来仔细拆一下。它背后牵出的哈希表标记法、快慢指针(Floyd 判圈算法)、边界条件处理、以及 JavaScript 里引用比较的细节,几乎能帮你把链表基础再夯实一圈。
我一开始做这道题的时候,其实第一反应是“拿一个数组把访问过的节点存下来”,这当然能过,但后来在面试场景里被问到“能不能用 O(1) 空间解决”时,才意识到快慢指针才是这道题真正的灵魂。下面我会把两种主流解法、背后的核心原理、JavaScript 实现时容易踩的坑、以及本地测试环境怎么写,全部展开聊聊。
1. 题目回顾与思路拆解:环到底是怎么形成的
1.1 题目描述与输入模型
Leetcode 141 的描述很简短:给你一个链表的头节点 head,判断链表中是否有环。评测系统内部会用一个整数 pos 表示链表尾节点连接到链表中的位置(索引从 0 开始),如果 pos 为 -1,则表示链表中没有环。
这里有一个初学者很容易被绕进去的点:pos 并不是一个会传给你的参数。你在 Leetcode 上写函数时,形参列表里只有 head,根本没有 pos。pos 只是评测系统在构造测试数据时使用的内部标记。也就是说,你需要依赖链表的结构本身去判断,而不是幻想着“我拿到 pos,那如果 pos >= 0 就直接返回 true”。
要从结构上理解环,可以这样想:正常的单链表是一条单行道,每个节点的 next 指向下一个节点,最后一个节点的 next 指向 null;而环形链表相当于某个节点的 next 又指回了之前的某个节点,于是遍历时会陷入死循环,永远走不到 null。比如 head = [3,2,0,-4], pos = 1 这个示例,最后一个节点 -4 的 next 指向了索引为 1 的节点 2,所以当你遍历到 -4 后,会跳回 2,然后继续走,形成环路。
在 JavaScript 里,Leetcode 环境默认给你提供的链表节点构造函数一般是这样的:
javascript复制function ListNode(val) {
this.val = val;
this.next = null;
}
不过我建议你自己本地测试时,用更现代的类写法:
javascript复制class ListNode {
constructor(val, next = null) {
this.val = val;
this.next = next;
}
}
1.2 实现之前需要先确定的几个问题
动手写代码之前,最好先把几个基础问题理清楚,不然很容易在边界条件上翻车:
-
返回值是什么?
这是一个布尔判断问题,要求返回 true 或 false,而不是返回环的入口节点,也不是返回环的长度。所以代码尾部只需要return true;或return false;。 -
空链表和单节点链表算有环吗?
如果 head 是 null,链表里根本没有节点,自然没有环。如果只有一个节点,且它的 next 为 null,也没有环。只有节点的 next 最终能指回链路中某一个节点时,才算有环。 -
题目有没有给节点数量的范围?
Leetcode 原题一般会说明链表中节点的数量范围是[0, 10^4]。也就是说节点数最多有一万个,递归或者某些非常笨重的写法可能会出问题,这也是需要考虑的。 -
不能用修改原链表的方式作弊
有些取巧做法会把访问过的节点的 next 指向一个特殊标记节点,或者把 val 改成某个固定值。虽然能在 Leetcode 上通过,但本质上破坏了输入数据,面试时也不推荐,甚至有些判题系统会检测到这种修改并导致错误。所以我下面只讲两种正规解法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解法一:哈希表标记法,最直观的“记住我来过”
2.1 核心思想与代码实现
第一种解法非常符合人的直觉:我一边遍历链表,一边把访问过的
