LeetCode 141:环形链表

LeetCode 141:环形链表------用快慢指针判断链表是否成环

一、题目描述

今天做的是 LeetCode 141:环形链表。

题目要求判断一个单链表中是否存在环。

所谓环,就是链表中的某个节点的 next 指针重新指向了前面的某个节点。

例如正常链表:

text 复制代码
1 → 2 → 3 → 4 → null

最终会走到 null,所以不存在环。

而如果:

text 复制代码
1 → 2 → 3 → 4
        ↑   ↓
        ← ←

链表就会一直循环下去。

要求返回:

text 复制代码
存在环 → true
不存在环 → false

二、我的思路:快慢指针

看到这道题,我想到的是链表中比较经典的:

快慢指针。

定义两个指针:

java 复制代码
ListNode fast = head;
ListNode slow = head;

其中:

text 复制代码
slow:每次走一步

fast:每次走两步

所以遍历的时候:

java 复制代码
fast = fast.next.next;
slow = slow.next;

如果链表没有环:

text 复制代码
slow
 ↓
1 → 2 → 3 → 4 → null

fast
 ↓

最终 fast 会走到 null。

而如果链表存在环,那么进入环以后:

text 复制代码
slow → →
fast → → →

虽然 fast 一开始领先 slow,但是因为两个人都在一个环里面不断移动,而且 fast 每次比 slow 多走一步,所以最终一定会追上 slow。

因此判断逻辑就很简单:

text 复制代码
fast 到 null
    ↓
没有环

fast == slow
    ↓
存在环

三、我的代码

最终代码:

java 复制代码
public class Solution {
    public boolean hasCycle(ListNode head) {

        // 链表为空,或者只有一个节点
        // 都不可能形成环
        if(head == null || head.next == null){
            return false;
        }

        ListNode fast = head;
        ListNode slow = head;

        while(fast != null && slow != null){

            // fast 如果已经没有下一个节点,
            // 说明链表到头了,不存在环
            if(fast.next == null){
                return false;
            }

            // fast 一次走两步
            fast = fast.next.next;

            // slow 一次走一步
            slow = slow.next;

            // 两个指针相遇,说明存在环
            if(fast == slow){
                return true;
            }
        }

        return false;
    }
}

四、为什么 fast 要检查 fast.next?

这里我觉得是代码中最需要注意的地方:

java 复制代码
if(fast.next == null){
    return false;
}

因为下面马上要执行:

java 复制代码
fast = fast.next.next;

如果:

text 复制代码
fast.next == null

那么继续访问:

java 复制代码
fast.next.next

就会出现空指针问题。

所以在让 fast 一次走两步之前,至少需要保证:

text 复制代码
fast.next 存在

也就是:

java 复制代码
if(fast.next == null){
    return false;
}

五、为什么 slow 不需要单独判断 slow.next?

代码中:

java 复制代码
while(fast != null && slow != null)

其实这里 slow != null 的判断有点多余。

原因是:

fast 的速度比 slow 快。

如果 slow 已经走到了 null,那么 fast 肯定也不可能还在正常链表中继续运行。

而真正可能出现的问题是:

java 复制代码
fast.next == null

因为 fast 一次要走两步。

所以代码实际上可以稍微简化成:

java 复制代码
while(fast != null && fast.next != null){
    fast = fast.next.next;
    slow = slow.next;

    if(fast == slow){
        return true;
    }
}

return false;

这是我更推荐记住的写法。


六、为什么两个指针相遇就一定有环?

这个问题是这道题最核心的地方。

假设链表没有环:

text 复制代码
1 → 2 → 3 → 4 → null

无论 fast 走得多快,最终都会走到 null。

所以:

text 复制代码
没有环 → fast 到 null

但是如果存在环:

text 复制代码
1 → 2 → 3 → 4
        ↑     ↓
        ← ← ←

当 slow 和 fast 都进入环以后,可以把它们理解成在一个跑道上跑步:

text 复制代码
slow:每次走 1 格
fast:每次走 2 格

那么每一轮:

text 复制代码
fast 比 slow 多走 1 格

假设环长度为 5,两个人之间相差 3 格。

那么:

text 复制代码
第 1 次:相差 2
第 2 次:相差 1
第 3 次:相差 0

最终就会相遇。

所以:

只要两个指针都进入同一个环,并且快指针比慢指针每次多走一步,那么快指针最终一定能够追上慢指针。

这就是 Floyd 判圈算法的核心。


七、这道题其实不需要额外的数据结构

如果使用 HashSet,也可以判断链表是否有环。

思路是:

text 复制代码
遍历节点
↓
把访问过的节点放进 Set
↓
如果某个节点已经存在
↓
说明出现重复
↓
存在环

但是这样需要保存访问过的节点:

text 复制代码
O(n)

而快慢指针只需要:

java 复制代码
ListNode fast;
ListNode slow;

所以额外空间复杂度是:

text 复制代码
O(1)

时间复杂度:

text 复制代码
O(n)

因此快慢指针更加适合这道题。


八、这次对快慢指针的理解

之前遇到链表题的时候,我更多把快慢指针理解成一种"固定套路":

text 复制代码
一个走一步
一个走两步

但这道题让我更加理解了它为什么能够解决问题。

关键其实不是:

"快指针和慢指针速度不同。"

而是:

当它们进入环以后,速度差会不断缩小它们之间的距离。

在环中:

text 复制代码
slow 每次 +1
fast 每次 +2

所以相对运动速度就是:

text 复制代码
2 - 1 = 1

只要环存在,最终一定会相遇。


九、总结

这道题的代码并不长,但它是一个非常经典的链表问题。

我的最终思路可以概括成:

text 复制代码
slow 每次走一步
fast 每次走两步
        ↓
如果 fast 到达 null
        ↓
没有环

如果 fast == slow
        ↓
存在环

最终复杂度:

text 复制代码
时间复杂度:O(n)
空间复杂度:O(1)

这道题最大的收获就是理解了快慢指针为什么能够判断环,而不只是记住:

java 复制代码
fast = fast.next.next;
slow = slow.next;

另外,自己的代码虽然可以通过,但:

java 复制代码
while(fast != null && slow != null)

中的 slow != null 实际上可以省略。

更常见、也更简洁的写法是:

java 复制代码
while(fast != null && fast.next != null){
    fast = fast.next.next;
    slow = slow.next;

    if(fast == slow){
        return true;
    }
}
return false;

这次我也算是把一个新的链表技巧真正记下来了:

快慢指针不仅可以用来找链表中点,也可以用来判断链表是否存在环。

相关推荐
IT枫斗者枫哥1 小时前
MyBatis一对多分页:LIMIT 20,为什么凑不齐20个订单?
java·数据库
代码方舟1 小时前
Java数据工程:利用天远全网运营商三要素优化线上实名认证合规体验
java·人工智能
BBmmo1 小时前
我的 Java 学习笔记 · 第 7 篇:泛型与通配符
java
独泪了无痕1 小时前
Hutool之RandomUtil:随机数生成的终极利器
java·后端
用户979984807181 小时前
200 行代码写一个能跑的 AI Agent:不依赖任何框架,只靠 tool-calling 原理
算法
花开路口1 小时前
深入理解 Java/Kotlin 协变与逆变
java·kotlin
运行时异常1 小时前
【WMS 仓储系统集成 AI Agent 实战】第 7 讲:Vue3 前端工程化——Token 刷新锁、Markdown 渲染踩坑与权限体系
java
threerocks1 小时前
【Muse实战】X 流量作战室搭建保姆级教程 - 拥有你自己的运营团队
算法
她的男孩1 小时前
开放接口限流从 20 改到 200 还是每分钟 20 次:拆完防重放+幂等+限流,我找到 5 个静默失效的坑
java·后端·架构
天天被压力1 小时前
【Python 量化取数指南 #13】Python 把行情落库:sqlite 一键存,回测随用随取
java·人工智能·python