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

目录


一、问题描述

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

也就是说:某个节点的 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) 步。

由于 fastslow 多绕了若干圈环,因此:

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

化简得:

复制代码
a = nb - x

这意味着什么?

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

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

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


3. 原理直观解释

可以这样理解:

  • slowfast 相遇时,它们都在环中;

  • 如果此时让一个指针从 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. 常见误区

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

四、总结对比

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

五、结语

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

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

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


六:力扣练习题


免责声明

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

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

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

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

相关推荐
2301_795167201 小时前
玩转Rust高级应用 如何避免对空指针做“解引用”操作,在C/C++ 里面就是未定义行为
c语言·c++·rust
智驱力人工智能3 小时前
基于视觉分析的人脸联动使用手机检测系统 智能安全管理新突破 人脸与手机行为联动检测 多模态融合人脸与手机行为分析模型
算法·安全·目标检测·计算机视觉·智能手机·视觉检测·边缘计算
2301_764441333 小时前
水星热演化核幔耦合数值模拟
python·算法·数学建模
循环过三天3 小时前
3.4、Python-集合
开发语言·笔记·python·学习·算法
priority_key6 小时前
排序算法:堆排序、快速排序、归并排序
java·后端·算法·排序算法·归并排序·堆排序·快速排序
不染尘.6 小时前
2025_11_7_刷题
开发语言·c++·vscode·算法
似水এ᭄往昔6 小时前
【C++】--stack和queue
开发语言·c++
仰望—星空7 小时前
MiniEngine学习笔记 : CommandListManager
c++·windows·笔记·学习·cg·direct3d
来荔枝一大筐7 小时前
力扣 寻找两个正序数组的中位数
算法
算法与编程之美8 小时前
理解Java finalize函数
java·开发语言·jvm·算法