LeetCode 入门级链表题------141. 环形链表,这道题是链表环判断的经典题型,考察对链表遍历和环结构的理解,同时有两种常用解题思路,适合新手巩固基础,也适合老手快速复盘,话不多说,直接开干!
一、题目解读
题目很简单:给你一个单链表的头节点 head,让你判断这个链表里面有没有"环"。
什么是链表中的环?举个例子:正常的链表是"一条路走到底",最后一个节点的 next 是 null,走到头就结束了;但如果有环,就相当于"走迷宫走到了死循环",某个节点的 next 会指向前面已经走过的节点,导致你沿着 next 指针一直走,永远走不到头。
这里要注意一个细节:题目里提到的 pos 参数,是系统用来标识环的位置的(比如 pos=2 表示链表尾连接到索引为2的节点),但我们写代码的时候不会用到这个参数,只需要通过链表本身的结构来判断是否有环即可。
最终要求:有环返回 true,无环返回 false。
二、前置知识
题目给出了单链表的节点定义,我们先看懂这个定义,后续代码才好理解:
typescript
class ListNode {
val: number
next: ListNode | null
constructor(val?: number, next?: ListNode | null) {
this.val = (val === undefined ? 0 : val) // 节点的值,默认0
this.next = (next === undefined ? null : next) // 指向的下一个节点,默认null(无下一个节点)
}
}
简单说:每个 ListNode 节点有两个属性,val 存值,next 存下一个节点的引用(没有就为 null)。我们遍历链表,就是通过不断访问 node.next 来实现的。
三、解题思路1:哈希表法
3.1 思路核心
利用哈希表存储我们已经遍历过的节点。遍历链表时,每遇到一个节点,就检查它是否已经在哈希表中:
-
如果在,说明我们之前已经走过这个节点了,现在又走回来了------存在环,直接返回 true;
-
如果不在,就把这个节点加入 Set,继续遍历下一个节点;
-
如果遍历到 node 为 null(走到链表末尾),说明没有环,返回 false。
这个思路的本质:用空间换时间,通过记录已走过的节点,避免重复遍历,从而判断是否有环。
3.2 完整代码
typescript
function hasCycle_1(head: ListNode | null): boolean {
const set = new Set(); // 用于存储已遍历的节点
let node: ListNode | null = head; // 遍历指针,从表头开始
while (node) { // 只要节点不为null,就继续遍历
if (set.has(node)) { // 检查当前节点是否已存在(走过)
return true; // 存在,说明有环
}
set.add(node); // 不存在,加入Set
node = node.next; // 移动到下一个节点
}
return false; // 遍历结束,没遇到重复节点,无环
}
3.3 思路解析&易错点
✅ 易错点1:Set 中存储的是「节点本身」,而不是节点的 val!因为不同节点可能有相同的 val,但只要节点引用不同,就不是同一个节点(比如两个 val=1 的节点,next 不同,就是两个独立节点)。如果存 val,会出现误判。
✅ 易错点2:遍历指针的初始值是 head,循环条件是 node(不是 node.next),因为如果 head 本身就是 null(空链表),直接返回 false,避免空指针报错。
✅ 时间复杂度:O(n),n 是链表的节点数,每个节点只遍历一次,Set 的 add 和 has 操作都是 O(1);
✅ 空间复杂度:O(n),最坏情况下(无环),需要存储所有 n 个节点。
四、解题思路2:快慢指针法
4.1 思路核心
这个思路也叫「龟兔赛跑」,是链表环判断的最优解------不需要额外空间,仅用两个指针就能实现。
定义两个指针,慢指针(slow)和快指针(fast),初始都指向 head:
-
慢指针每次走 1 步(slow = slow.next);
-
快指针每次走 2 步(fast = fast.next.next);
-
遍历链表,如果链表中存在环,那么快慢指针一定会在环内相遇(就像跑步时,快的人绕圈跑,总会追上慢的人);
-
如果链表中没有环,快指针会先走到链表末尾(fast 或 fast.next 为 null),此时返回 false。
4.2 完整代码
typescript
function hasCycle_2(head: ListNode | null): boolean {
let slow = head; // 慢指针,初始指向表头
let fast = head; // 快指针,初始指向表头
while (fast && fast.next) { // 快指针能走两步,避免空指针报错
slow = slow ? slow.next : null; // 慢指针走1步(处理slow为null的极端情况)
fast = fast.next.next; // 快指针走2步
if (slow === fast) { // 快慢指针相遇,说明有环
return true;
}
}
return false; // 快指针走到末尾,无环
}
4.3 思路解析&易错点
✅ 核心疑问:为什么有环的情况下,快慢指针一定会相遇?
简单解释:假设环的长度为 L,当慢指针刚进入环时,快指针已经在环内某个位置了。之后,快指针每次比慢指针多走 1 步(2步 - 1步),相当于快指针在以"每次1步"的速度追赶慢指针。因为环是闭合的,没有尽头,所以快指针一定会追上慢指针,也就是两者相遇。
✅ 易错点1:循环条件是 fast && fast.next,而不是 slow && fast!因为快指针走得快,如果 fast 是 null,或者 fast.next 是 null,说明快指针已经走到末尾,再走一步就会报错,此时直接判断无环。
✅ 易错点2:slow = slow ? slow.next : null 的处理。虽然初始时 slow 和 fast 都是 head,且循环条件保证了 fast 和 fast.next 不为 null,但极端情况下(比如 head 是 null),slow 可能为 null,加上这个判断可以避免空指针报错(简化写法也可以直接写 slow = slow.next,因为循环条件会保证 slow 不会提前为 null)。
✅ 时间复杂度:O(n),n 是链表节点数。最坏情况下,慢指针走完整个链表,快指针在环内多绕几圈,但总体还是线性时间;
✅ 空间复杂度:O(1),只用到了两个指针,没有额外开辟空间,比哈希表法更优。
五、两种思路对比(选择建议)
| 解题方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 哈希表法(hasCycle_1) | O(n) | O(n) | 新手入门、不需要优化空间、想快速写出正确代码 |
| 快慢指针法(hasCycle_2) | O(n) | O(1) | 面试最优解、空间受限场景、进阶优化 |
六、总结&拓展
这道题的核心是「判断链表是否存在环」,两种思路各有优劣,但快慢指针法是面试中的重点,一定要掌握其原理(龟兔赛跑的逻辑)。