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;

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

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

相关推荐
leobertlan43 分钟前
痛苦系列 | DSP-03 跨越单位圆:Z变换与零极点分析
算法·程序员
蛋蛋的就会顺顺的1 小时前
hot100 p0——栈,贪心算法,动态规划
java·前端·javascript·算法·leetcode·力扣
需要8262 小时前
Spring Bean 生命周期:从实例化到销毁的完整钩子
java·后端·spring
开发小能手1232 小时前
答案daanC语言
数据结构·算法
小 黄 鸡2 小时前
HashMap 面试必会知识点详解(JDK 8)
java·面试·职场和发展
Navigator_Z3 小时前
LeetCode //C - 1291. Sequential Digits
c语言·算法·leetcode
伯恩bourne4 小时前
Spring Boot Bean 注册的两种方式:@Component 家族 vs @Configuration + @Bean
java·spring boot·后端
silvamux_4 小时前
curl 测试 OpenAI 兼容 API 的完整流程
java·人工智能·缓存·ai·oneapi
pride.li4 小时前
第03章 Python 教程-基础语法:注释与变量
服务器·前端·后端·python·算法·计算机视觉
省钱兄--zs4 小时前
深圳24小时自助健身房软硬件解决方案实战指南与开发流程详解
java·数据仓库·spring boot·小程序·架构