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

目录


一、问题描述

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

也就是说:某个节点的 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 判圈算法不仅是链表问题的高频面试题,更是一种"数学与编程的完美结合"。

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

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


六:力扣练习题


免责声明

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

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

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

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

相关推荐
_OP_CHEN6 小时前
C++基础:(十五)queue的深度解析和模拟实现
开发语言·c++·stl·bfs·queue·容器适配器·queue模拟实现
Swift社区6 小时前
LeetCode 402 - 移掉 K 位数字
算法·leetcode·职场和发展
起床气2336 小时前
C++海战棋开发日记(序)
开发语言·c++
_码力全开_6 小时前
P1005 [NOIP 2007 提高组] 矩阵取数游戏
java·c语言·c++·python·算法·矩阵·go
墨染点香6 小时前
LeetCode 刷题【124. 二叉树中的最大路径和、125. 验证回文串】
算法·leetcode·职场和发展
Camel卡蒙6 小时前
红黑树详细介绍(五大规则、保持平衡操作、Java实现)
java·开发语言·算法
AhriProGramming6 小时前
Flask-SQLAlchemy精读-双语精选文章
python·算法·flask