一文彻底理解:如何判断单链表是否成环(含原理推导与环入口推算)

目录


一、问题描述

给定一个单链表,判断其中是否存在环。

也就是说:某个节点的 next 指针是否指向了前面的节点,导致链表无限循环。

示意图:

复制代码
1 -> 2 -> 3 -> 4 -> 5
          ^         |
          |_________|

上面的链表从节点 5 指向节点 3,因此形成了一个环。


二、常见解法

方法一:快慢指针(Floyd 判圈算法)

  • 定义两个指针:

    • slow:一次走一步;
    • fast:一次走两步;
  • 如果链表无环,fast 最终会遇到 nullptr;

  • 如果链表有环,fast 一定会在环中追上 slow(它们会在环内相遇)。

cpp 复制代码
struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

bool hasCycle(ListNode* head) {
    if (!head || !head->next) return false;

    ListNode* slow = head;
    ListNode* fast = head;

    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;

        if (slow == fast)
            return true;
    }

    return false;
}
项目 复杂度
时间复杂度 O(n)
空间复杂度 O(1)

方法二:哈希表法(直观但耗空间)

用哈希表记录访问过的节点指针,如果访问到重复节点说明有环。

cpp 复制代码
#include <unordered_set>

bool hasCycle(ListNode* head) {
    std::unordered_set<ListNode*> visited;
    while (head) {
        if (visited.count(head))
            return true;
        visited.insert(head);
        head = head->next;
    }
    return false;
}
项目 复杂度
时间复杂度 O(n)
空间复杂度 O(n)

三、进阶问题:如何找到环的起点?

判断出"有环"之后,很多面试题会进一步问:

如果存在环,如何找到环的入口节点?

这一部分正是 Floyd 判圈算法的精髓。


1. 数学符号定义

假设链表由两部分组成:

复制代码
[非环部分] 长度 = a
[环部分]   长度 = b

假设相遇点距离环入口的距离为 x。

也就是说:

复制代码
head --a--> 入口 --x--> 相遇点 --(b - x)--> 入口

我们设:

  • slow 每次走 1 步;
  • fast 每次走 2 步。

2. 相遇时的关系推导

当两者第一次相遇时:

  • slow 走了 a + x 步;
  • fast 走了 2(a + x) 步。

由于 fast 比 slow 多绕了若干圈环,因此:

复制代码
2(a + x) - (a + x) = nb    ->    a + x = nb

化简得:

复制代码
a = nb - x

这意味着什么?

从相遇点出发,走 nb - x 步,就会回到环的入口。

而因为环的长度是 b,所以"绕 n 圈"是等价的。

因此我们也可以说:从相遇点再走 (b - x) 步,就能回到环的入口。


3. 原理直观解释

可以这样理解:

  • 当 slow 与 fast 相遇时,它们都在环中;

  • 如果此时让一个指针从 head 开始走,另一个从 相遇点 开始走;

  • 两者速度相同,每次走一步:

    • 从头出发的指针需要走 a 步到达环入口;
    • 从相遇点出发的指针需要走 b - x 步回到环入口;
  • 由于根据推导公式 (a = nb - x),它们会在环的起点重合。


4. 代码实现

cpp 复制代码
ListNode* detectCycle(ListNode* head) {
    ListNode* slow = head;
    ListNode* fast = head;

    // 第一步:判断是否有环
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) {
            // 第二步:有环,寻找环入口
            slow = head;
            while (slow != fast) {
                slow = slow->next;
                fast = fast->next;
            }
            return slow; // 环的入口
        }
    }

    return nullptr; // 无环
}

5. 例子说明

假设链表如下:

复制代码
a = 3(非环长度)
b = 5(环的长度)

链表结构:

复制代码
head(0) → 1 → 2 → 3 → 4 → 5 → 6 → 7 → (回到3)
  • slow 每次走一步;
  • fast 每次走两步;
  • 它们第一次相遇在节点 6;
  • 从头走 a=3 步可以到达入口节点 3;
  • 从节点 6 再走 b-x=3 步(6→7→3)也正好到达节点 3。

两者在环入口相遇。


6. 文字图解

下面用 ASCII 示意整个过程:

复制代码
head
 ↓
[节点1] → [节点2] → [节点3] → [节点4] → [节点5]
                           ↑                     ↓
                           ← ← ← ← ← ← ← ← ← ← ←
            ↑
            |------ 非环长度 a ------|

在相遇点:
- slow 在环内
- fast 在环内,快指针比慢指针多走若干圈
当让 slow 回到 head 后,两者再以相同速度前进:
- 一个从外部走进环
- 另一个从环内走向入口
它们在入口节点重合

7. 常见误区

误区 正确理解
"为什么不是直接在相遇点就是环入口?" 相遇点可能在环内任意位置,取决于 a 和 b。
"为啥一定会在入口相遇?" 因为两者速度相同,且它们相对位置差等于 a,环长是周期结构。
"n 可以是几?" n 是 fast 多绕的圈数,通常为 1,但数学上不影响结论。

四、总结对比

方法 能判断有环 能找入口 时间复杂度 空间复杂度 思想核心
哈希表 ✅ ✅ O(n) O(n) 记录访问节点
快慢指针(Floyd) ✅ ✅ O(n) O(1) 相对速度+数学推导

五、结语

Floyd 判圈算法不仅是链表问题的高频面试题,更是一种"数学与编程的完美结合"。

它展示了空间最优解法 如何通过逻辑关系实现"无痕检测"。

理解它的推导过程,比死记硬背更重要------这样在面对复杂数据结构题目时,你能举一反三。


六:力扣练习题


免责声明

本文内容仅供学习与参考,旨在帮助读者理解相关算法与编程思想。

文中代码、图示及思路均为学习笔记整理,可能存在疏漏或不足之处。

如在实际应用中出现错误或与特定环境不符,请以实际情况、官方文档或权威资料为准。

封面图来源于网络,如有侵权,请联系删除!

相关推荐
jimy16 小时前
c++的 Type Traits (一)
开发语言·c++
别动我齐刘海7 小时前
Linux 系统调试学习2
linux·运维·c++·人工智能·学习·机器学习·机器人
171320330计算机毕设编程7 小时前
计算机毕业设计 基于YOLO改进算法的车辆特征识别系统设计与实现--lw
java·大数据·python·sql·算法
姜鱼问生7 小时前
Nginx gzip 压缩:一个配置让页面加载快 3 倍
后端·算法·架构
不会就选b7 小时前
算法日常・每日刷题--<动态规划>23
算法·动态规划
GreatSQL社区8 小时前
主从复制SQL线程中断——字符集砸出的坑
java·数据库·算法
ZZHow10248 小时前
Ascend C算子开发(高级)笔记
c++·人工智能·笔记·深度学习·华为云
Tisfy9 小时前
LeetCode 2333.最小差值平方和:排序 + 数学计算(不用二分)
数学·算法·leetcode·题解·贪心·排序
aqiu1111119 小时前
【C++算法打怪专栏】LeetCode 217. 存在重复元素(解法总结)
c++·算法·leetcode