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;

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

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

相关推荐
灯澜忆梦12 小时前
【面向对象编程c++】| 指针与自定义数据类型
开发语言·c++·算法
十年Java程序媛12 小时前
Java 多线程基础与锁机制详解|进程线程、锁升级、synchronized 与 Lock 对比
java·开发语言·spring boot
弯_弯12 小时前
FDE 的工程能力画像:数据接入、PoC 与可复用资产的落地口径
java·服务器·开发语言
xxwl58512 小时前
区域赛算法模版第八章---贪心(Greedy)
c++·笔记·学习·算法
CCblandy13 小时前
向量检索 ANN 参数实测:换一种数据分布,recall 掉了 51 个百分点
人工智能·python·算法·自然语言处理·性能优化
CS创新实验室13 小时前
一棵树怎么做到“自平衡还不太费力“?——红黑树的旋转与染色
数据结构·算法·红黑树·408·计算机考研
Logic10113 小时前
C语言/数据结构位运算题解:异或XOR找出被挑选一次的果子编号——只出现一次的数字
c语言·数据结构·算法·数组·位运算·时间复杂度·空间复杂度
深圳市快瞳科技有限公司13 小时前
面向端侧部署的多帧宠物身份识别:完整性评分机制与工程约束
人工智能·算法·计算机视觉·端侧ai·宠物识别·宠物ai·宠物身份识别
云和数据.ChenGuang13 小时前
print(f“a+b=“,result) print(f“a+b={result}“)区别
java·前端·人工智能·python·深度学习
2601_9628857214 小时前
如何用 Python 算最大回撤和回撤恢复期?
java·数据库·python