
本文详解单链表检测并移除环时,因环起点为头节点导致 `prev` 为 `null` 而引发 `nullpointerexception` 的根本原因,并提供带哨兵节点(dummy node)的标准修复方案,确保算法对任意环位置(包括自环、头环)均鲁棒有效。
在使用 Floyd 判圈算法(快慢指针)检测并移除链表环时,一个常见但易被忽视的问题是:当环的入口节点恰好是链表头节点(即 Head 自身在环中)时,标准的“找前驱断链”逻辑会失效。问题代码中,removeCycle() 方法在第二阶段尝试用 prev 记录 fast 的前驱节点,但若环入口为 Head,则 slow 与 fast 在首次相遇后立即进入 while(slow != fast) 循环——此时 slow == Head,fast == Head,循环体一次都不执行,prev 始终为 null,最终 prev.next = null 触发空指针异常。
根本原因在于:原算法假设环入口非头节点,使得 slow 和 fast 在重置后需至少移动一次才能相遇,从而保证 prev 被赋值;但当入口为头节点时,二者初始即相等,跳过整个循环,prev 未初始化。
✅ 正确解法:引入哨兵节点(dummy node)
在链表头部插入一个临时哑节点,将原 Head 变为 dummy.next。这样,即使环入口是原 Head,在新结构中它也变为“非头节点”,slow 与 fast 必然需要移动才能相遇,prev 必然被正确赋值。哨兵节点仅在方法内存在,退出后自动被垃圾回收,零副作用。
以下是修复后的完整实现:
public static int removeCycle() {
// 创建哨兵节点,避免头节点为环入口时 prev 未初始化
Node dummy = new Node(0);
dummy.next = Head;
Node slow = dummy;
Node fast = dummy;
boolean hasCycle = false;
// 第一阶段:Floyd 检测环
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
hasCycle = true;
break;
}
}
if (!hasCycle) {
return 0; // 无环
}
// 第二阶段:定位环入口的前驱节点
slow = dummy;
Node prev = null;
while (slow != fast) {
prev = fast; // prev 始终指向 fast 的前一个节点(在移动前)
slow = slow.next;
fast = fast.next;
}
// 此时 prev 是环入口节点的前驱,断开环
prev.next = null;
return 1;
}⚠️ 注意事项:
立即学习“Java免费学习笔记(深入)”;
- 主函数调用验证需修正:原代码 System.out.println(Head.next.next.next.next.data) 试图访问 null 的 data 字段,必然抛异常。应改为打印引用本身(如 System.out.println(Head.next.next.next.next);),输出 null 即表示断链成功。
- 哨兵节点生命周期安全:dummy 是局部变量,方法返回后不可达,JVM 会自动回收,无需手动处理。
- 时间/空间复杂度:仍为 O(n) 时间、O(1) 空间,未增加额外开销。
总结:处理链表环问题时,必须覆盖所有边界场景。哨兵节点是解决“头节点为环入口”这一经典陷阱的简洁、通用且工程友好的方案。它不改变原链表结构语义,仅在算法执行期提供统一入口,是链表操作中值得熟练掌握的设计模式。










