网站首页 活动公告 礼包中心 攻略指南
首页 >> 攻略指南
一文彻底理解:如何判断单链表是否成环(含原理推导与环入口推算)

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

目录 一、问题描述 二、常见解法 [方法一:快慢指针(Floyd 判圈算法)](#方法一:快慢指针(Floyd 判圈算法)) 方法二:哈希表法(直观但耗空...

目录

一、问题描述

二、常见解法

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

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

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

[1. 数学符号定义](#1. 数学符号定义)

[2. 相遇时的关系推导](#2. 相遇时的关系推导)

[3. 原理直观解释](#3. 原理直观解释)

[4. 代码实现](#4. 代码实现)

[5. 例子说明](#5. 例子说明)

[6. 文字图解](#6. 文字图解)

[7. 常见误区](#7. 常见误区)

四、总结对比

五、结语

六:力扣练习题

一、问题描述

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

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

bool hasCycle(ListNode* head) {

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

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

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

六:力扣练习题

LeetCode 141. Linked List Cycle

力扣 - 环形链表

LeetCode 142. Linked List Cycle II

力扣 - 环形链表II

免责声明

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

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

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

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