链表找环:Floyd 判圈算法(LeetCode 141 & 142 )

在链表相关的算法题中,「判断链表是否有环」以及「寻找入环点」是两道极其经典的题目。它们不仅考察了对指针的操作,更蕴含了一个巧妙的数学原理------Floyd 判圈算法(龟兔赛跑算法)。

一、 LeetCode 141:判断链表中是否有环(题目链接)

题目要求: 给定一个链表,判断链表中是否有环。

1. 核心思想:快慢指针(龟兔赛跑)

如果在直线跑道上,跑得快的人一定会先到达终点;但如果是在环形跑道上,跑得快的人最终一定会从背后追上(套圈)跑得慢的人。

我们设定两个指针:

慢指针 slow**:** 每次移动 1 步。

快指针 fast**:** 每次移动 2 步。

2. 逻辑推演

无环情况: fast 指针会率先走到链表末尾(nullptr),此时循环结束,返回 false。

有环情况: fast 指针进入环后,会在环内不断循环。由于 fast 每次比 slow 多走 1 步,两者的距离会不断缩小,最终必定在环内某处相遇(slow == fast),此时返回 true。

3. 代码实现 (C++)

cpp 复制代码
class Solution {
public:
    bool hasCycle(ListNode *head) {
        if (head == nullptr || head->next == nullptr) return false;
        
        ListNode *slow = head;
        ListNode *fast = head;
        
        while (fast != nullptr && fast->next != nullptr) {
            slow = slow->next;       // 慢指针走一步
            fast = fast->next->next; // 快指针走两步
            
            if (slow == fast) return true; // 相遇即有环
        }
        return false;
    }
};

复杂度分析: 时间复杂度 O(N),空间复杂度 O(1)。

二、 LeetCode 142:寻找入环的第一个节点(题目链接)

题目要求: 如果链表有环,返回入环的第一个节点;无环则返回 null。

这道题在 141 的基础上增加了难度,不仅要知道有没有环,还要精准定位环的入口。这需要用到一段精妙的数学推导。

1. 变量定义

假设从头节点到入环点的距离为 a,从入环点到快慢指针相遇点的距离为 b,从相遇点再回到入环点的距离为 c。

环的总长度为 b + c。

2. 数学证明(为什么相遇后,从头走和从相遇点走会碰头?)

当快慢指针相遇时:

慢指针 slow走过的路程: a + b

快指针 fast走过的路程: a + b + n * (b + c) (n 为快指针在环内转的圈数,n >= 1)

因为快指针的速度是慢指针的两倍,所以相同时间内,快指针走过的路程是慢指针的 2 倍:

cpp 复制代码
2 * (a + b) = a + b + n * (b + c)

化简等式:

cpp 复制代码
a + b = n * (b + c)
a = n * (b + c) - b
a = (n - 1) * (b + c) + c

结论解读:

(n - 1) * (b + c) 代表在环内转了 (n-1) 个整圈。这说明:

从头节点走到入环点的距离 a**,等于从相遇点走到入环点的距离** c**(加上转了若干整圈)。**

3. 算法设计

基于上述数学结论,我们可以在快慢指针相遇后,开启第二阶段:

让一个指针 ptr 重新回到头节点 head。

让 ptr 和相遇点的 slow 指针同时出发,每次各走 1 步。

由于 a 的长度等同于 c(加上整数圈),这两个指针必定会在入环点相遇。

4. 代码实现 (C++)

cpp 复制代码
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        if (head == nullptr || head->next == nullptr) return nullptr;
        
        ListNode *slow = head;
        ListNode *fast = head;
        
        while (fast != nullptr && fast->next != nullptr) {
            slow = slow->next;
            fast = fast->next->next;
            
            if (slow == fast) { // 1. 找到相遇点
                ListNode *ptr = head; // 2. 指针回到头节点
                
                // 3. 两指针同速前进,相遇点即为入环点
                while (ptr != slow) {
                    ptr = ptr->next;
                    slow = slow->next;
                }
                return ptr; 
            }
        }
        return nullptr; // 无环
    }
};

复杂度分析: 时间复杂度 O(N),空间复杂度 O(1)。

相关推荐
专业程序开发源1 小时前
flask家电故障预测系统92491-计算机课程设计/毕业设计
vscode·python·sql·算法·flask·课程设计
liulilittle1 小时前
Linux 下 select 测试函数
linux·服务器·网络·数据库·c++·select·c
Logic1011 小时前
C语言/数据结构欧几里得算法题解:长方形切割最大正方形——贪心划分计数
c语言·数据结构·算法·贪心算法·时间复杂度·空间复杂度·欧几里得算法
yi0112 小时前
LeetCode 643. 子数组最大平均数 I|从暴力枚举到滑动窗口
人工智能·笔记·python·算法·leetcode·滑动窗口
无忧.芙桃2 小时前
C++语言原理与实践(十二):list类的底层实现
c++·windows·list
沫璃染墨2 小时前
《Linux工程实践篇(二):线程池实战——从线程安全单例到完整源码实现》
linux·开发语言·c++·单例模式·设计模式·系统架构
极光通讯2 小时前
服务器内存来料检验(IQC)实操:批次追溯、外观判定与上机验证
java·服务器·算法
淡海水2 小时前
02-04-原理篇-GC Roots与对象图遍历
算法·unity·c#·图·gc·遍历·roots
ClickHouseDB2 小时前
ClickHouse 大规模 OpenTelemetry 数据接入架构的可靠性演进
算法