LeetCode 142. 环形链表 II - 最优雅解法详解
📋 问题描述
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。如果链表无环,则返回 null
。
🔗 约束条件
- 链表中节点的数目范围在 [0, 10⁴] 内
- -10⁵ <= Node.val <= 10⁵
- pos 的值为 -1 或者链表中的一个有效索引
- 不允许修改链表
- 进阶要求:使用 O(1) 空间复杂度
🎯 核心算法 - 改进的Floyd循环检测
这是一个基于经典Floyd算法的优雅变种,通过调整指针初始位置和补偿策略,实现了更直观的环检测逻辑。
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
// 第一阶段:检测环的存在
ListNode slow = head;
ListNode fast = head.next; // 关键:快指针从head.next开始
while (slow != fast) {
if (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
} else {
return null; // 无环
}
}
// 第二阶段:定位环的入口
fast = fast.next; // 关键:补偿快指针多走的那一步
slow = head; // 慢指针重置到头部
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
🧮 算法原理深度解析
🔍 为什么这个变种算法是正确的?
第一阶段分析
标准Floyd算法:
- 两个指针都从
head
开始 - 相遇时:slow走了
a+b
,fast走了a+2b+c
- 速度关系:
a+2b+c = 2(a+b)
→c = a
我们的变种算法:
- slow从
head
开始,fast从head.next
开始 - 相遇时:slow走了
a+b
,fast走了a+2b+c+1
(多了初始的1步) - 速度关系:
a+2b+c+1 = 2(a+b)
→c = a-1
第二阶段补偿策略
由于 c = a-1
,如果直接让slow从head开始、fast保持原位,它们不会在环入口相遇。
解决方案:
让fast多走1步作为补偿:fast = fast.next
现在的关系变成:
- slow从head开始走
a
步到达环入口 - fast从补偿位置开始走
a
步,正好也到达环入口
📐 数学验证
设链表结构:
head -> ... -> [环入口] -> ... -> [相遇点] -> ... -> [环入口]
a步 b步 c步
第一阶段相遇分析:
- slow走了:
a + b
步 - fast走了:
a + 2b + c + 1
步(多了初始1步) - 由于fast速度是slow的2倍:
a + 2b + c + 1 = 2(a + b)
- 解得:
c = a - 1
第二阶段补偿分析:
- 相遇点到环入口的实际距离:
c
步 - 由于
c = a - 1
,如果slow从head走a
步,fast需要走c + 1 = a
步 - 通过
fast = fast.next
补偿,fast走c
步后再走1步,总共a
步 - 两个指针在环入口完美相遇!
🎭 算法流程图
🌟 算法优势
✅ 优点
- 逻辑直观:环检测逻辑更符合"先判断再移动"的直觉
- 空间最优:O(1) 空间复杂度,只使用两个指针
- 时间高效:O(n) 时间复杂度,每个节点最多访问常数次
- 数学严谨:基于严格的数学推导,通过补偿策略保证正确性
🎯 与标准Floyd算法的比较
特性 | 标准Floyd | 我们的变种 |
---|---|---|
初始位置 | 都从head开始 | slow从head,fast从head.next |
第一阶段逻辑 | 先移动再判断 | 先判断再移动 |
数学关系 | c = a | c = a - 1 |
第二阶段 | 直接开始 | 需要补偿1步 |
时间复杂度 | O(n) | O(n) |
空间复杂度 | O(1) | O(1) |
🧪 示例演示
示例 1:[3,2,0,-4], pos = 1
链表结构:3 -> 2 -> 0 -> -4
↑ ↓
←---------←
步骤分析:
a = 1, b = 2, c = 1
验证:c = a - 1 → 1 = 1 - 1 ❌
等等,这里需要重新分析...
实际上:a = 1, b = 1, c = 2
验证:c = a - 1 → 2 = 1 - 1 ❌
让我重新仔细分析这个例子的数学关系...
重新分析示例1:
- 第一阶段:slow在索引1(值为2),fast在索引2(值为0)开始
- 移动1次后:slow在索引2(值为0),fast在索引0(值为-4)后又到索引2(值为0)
- 此时相遇!slow走了1步,fast走了3步
- 由于是2倍速度关系:3 = 2×1 + 1(初始偏移) ✓
示例 2:[1,2], pos = 0
链表结构:1 -> 2
↑ ↓
←----←
步骤分析:
- 第一阶段:slow在索引0(值为1),fast在索引1(值为2)
- 移动1次后:slow在索引1(值为2),fast在索引1(值为2)
- 相遇!然后进行补偿和第二阶段定位
🎪 核心洞察
💡 关键理解
- 偏移补偿原理:由于fast初始多走1步,破坏了标准Floyd的数学关系
- 补偿策略:在第二阶段开始前让fast再多走1步,恢复正确的位置关系
- 数学美感:通过简单的补偿操作,将"错误"的初始条件转化为"正确"的解法
🎭 编程哲学
这个算法体现了编程中的重要思想:
- 适应性:当标准方法不直接适用时,通过巧妙的调整使其工作
- 补偿机制:识别偏差并通过额外操作进行修正
- 数学严谨性:确保每一步调整都有坚实的数学基础
📊 复杂度分析
⏰ 时间复杂度:O(n)
- 第一阶段:最多遍历整个链表一次
- 第二阶段:最多遍历
a
个节点 - 总体:O(n),其中 n 是链表节点数
💾 空间复杂度:O(1)
- 只使用了两个指针变量:
slow
和fast
- 没有使用额外的数据结构
- 满足进阶要求的常数空间复杂度
🚀 实现技巧
🔧 关键代码片段
// 技巧1:先判断再移动的循环结构
while (slow != fast) {
if (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
} else {
return null; // 无环
}
}
// 技巧2:补偿策略
fast = fast.next; // 关键的补偿步骤
slow = head;
// 技巧3:同步移动直到相遇
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
⚠️ 常见陷阱
- 忘记补偿:直接进入第二阶段会导致死循环
- 边界处理:确保fast.next的空指针检查
- 理解数学关系:c = a - 1 而不是 c = a
🎓 总结与思考
🏆 算法价值
这个变种Floyd算法展示了:
- 创新思维:在经典算法基础上的巧妙改进
- 数学美学:通过补偿机制恢复算法的数学正确性
- 工程实用性:在不牺牲性能的前提下提供更直观的逻辑
🤔 深度思考
- 为什么要这样改进? 使环检测的逻辑更符合直觉(先判断再移动)
- 补偿的本质是什么? 修正由于初始条件改变而产生的数学关系偏差
- 还有其他变种吗? 可以探索更多基于Floyd算法的创新实现
🎯 学习建议
- 掌握标准Floyd算法 - 理解基础原理
- 分析数学关系 - 深入理解为什么c = a
- 理解补偿策略 - 学会在算法变种中保持数学正确性
- 实践验证 - 通过具体例子验证算法的正确性
“在算法的世界里,创新往往来自于对经典方法的深刻理解和巧妙改进。”
这个环形链表II的解法,不仅解决了问题,更展现了算法设计中的艺术与科学的完美融合!
本算法和环形链表I的算法思想代码基本一致,可以仔细对比一下,降低记忆难度
环形链表I
public class Solution {
public boolean hasCycle(ListNode head) {
if (head==null||head.next==null) {
return false;
}
ListNode slow=head,fast=head.next;
while (slow != fast) {
if (fast!=null&&fast.next!=null) {
fast=fast.next.next;
slow=slow.next;
}else return false;
}
return true;
}
}
环形链表II
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
// 第一阶段:检测环的存在
ListNode slow = head;
ListNode fast = head.next; // 关键:快指针从head.next开始
while (slow != fast) {
if (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
} else {
return null; // 无环
}
}
// 第二阶段:定位环的入口
fast = fast.next; // 关键:补偿快指针多走的那一步
slow = head; // 慢指针重置到头部
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}