1290 字
6 分钟
判断单链表环并找出环起点:Floyd 判圈算法

题目描述#

子问题一:判断链表是否包含环#

给定一个单链表的头节点 head,判断链表中是否存在环。如果链表中有某个节点可以通过连续跟踪 next 指针再次到达,则链表中存在环。

输入:head = 3→2→0→-4
↑_______↓
输出:true

子问题二:找出环的起点#

在上题基础上,如果存在环,返回环的入口节点

输入:head = 3→2→0→-4
↑_______↓
输出:节点 2
解释:环的入口是节点 2

解题思路#

第一阶段:Floyd 判圈(判断有无环)#

使用快慢指针:slow 每次走一步,fast 每次走两步。

  • 如果链表无环:fast 会先到达 null,判定无环
  • 如果链表有环:fast 和 slow 一定会在环中相遇(就像在圆形操场上,快的人总会套圈慢的人)

第二阶段:找出环的入口(数学推导)#

当 fast 和 slow 在环中相遇后:

  1. 将 slow 重新指向链表头部
  2. slow 和 fast 以相同的速度(每次一步)前进
  3. 两者再次相遇的节点就是环的入口

数学证明#

设:

  • 链表头部到环入口的距离为 a
  • 环入口到第一次相遇点的距离为 b
  • 环的长度为 L,则相遇点到环入口的剩余距离为 c = L - b

第一次相遇时:

  • slow 走了 a + b
  • fast 走了 a + b + n·L 步(n 为 fast 多绕的圈数)

因为 fast 速度是 slow 的 2 倍:2(a + b) = a + b + n·La + b = n·La = n·L - b = (n-1)·L + (L - b) = (n-1)·L + c

a = c + (n-1)·L —— 这意味着从链表头部走 a 步到环入口的距离,等于从相遇点走 c 步到环入口(可能多绕几圈)的距离。

所以,让 slow 从头出发,fast 从相遇点出发,两者同速前进,必定在环入口相遇。

代码实现#

C
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct ListNode {
int val;
struct ListNode *next;
} ListNode;
/* 检测是否有环 */
bool hasCycle(ListNode *head) {
ListNode *slow = head;
ListNode *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) return true; // 快慢指针相遇,有环
}
return false; // fast 到达末尾,无环
}
/* 寻找环的入口节点 */
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 NULL; // 无环
}
C++
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
class Solution {
public:
// 检测是否有环
bool hasCycle(ListNode *head) {
ListNode *slow = head;
ListNode *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) return true;
}
return false;
}
// 寻找环的入口节点
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;
}
};
JavaScript
class ListNode {
constructor(val) {
this.val = val;
this.next = null;
}
}
// 检测是否有环
function hasCycle(head) {
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
}
// 寻找环的入口节点
function detectCycle(head) {
let slow = head;
let 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 null;
}

图解过程#

第一阶段:判圈#

链表:3 → 2 → 0 → -4
↑__________↓
Step 1: slow=3, fast=3
Step 2: slow=2, fast=0
Step 3: slow=0, fast=2 ← fast 绕了一圈,套圈 slow
Step 4: slow=-4, fast=-4 ← 在节点 -4 相遇!有环!

第二阶段:找环入口#

第一次相遇后:
slow 重置到头部 (3)
fast 留在相遇点 (-4)
以相同速度前进:
Step 1: slow=2, fast=2
↓ ↓
两者在节点 2 相遇 → 节点 2 就是环的入口!

复杂度分析#

维度分析
时间复杂度O(n) — slow 最多走 n 步(从头到入口再到相遇点),两阶段各 O(n)
空间复杂度O(1) — 只用了两个指针

关键要点#

  1. Floyd 判圈的核心是速度差:只要 fast 比 slow 快(不一定是 2 倍),且速度差与环长互质,就能检测到环。选择 2 倍是工程最优实践。

  2. 数学证明是面试亮点:能推导出 a = c + (n-1)·L 是区分”背模板”和”真理解”的关键。

  3. 等速回找的优雅:第二阶段的 slow 和 fast 同速走,相遇即入口——这是 Floyd 算法最美妙的部分。

扩展思考#

  • 如果要求计算环的长度,如何做?(相遇后 fast 不动,slow 继续走一圈计数)
  • 快指针走 3 步行不行?有什么影响?

相关文章:

判断单链表环并找出环起点:Floyd 判圈算法
https://www.hehonglei.cn/technology/linked-list-cycle/
作者
Honglei He
发布于
2026-08-10
许可协议
CC BY-NC-SA 4.0