题目描述

✅ 面试题 02.08. 环路检测

image-20260928224125545

image-20260928224125547

题意分析

若沿 next 能再次回到某个已经访问过的节点,链表就存在环,需要返回从头节点出发第一次进入环的那个节点;不存在环则返回空。返回的是原节点引用,节点值相同不代表它们是同一个节点。

题目中的 pos 只用来描述尾部接回的位置,不会作为参数传入。要满足常数额外空间的要求,可以先用快慢指针确认有环,再由相遇位置推导环入口。

解法:快慢指针

核心思路

[!blue]

先判断是否有环。令 slow、fast 都从头节点出发,每轮分别走一步、两步,再比较节点引用。如果没有环,快指针最终会到达空指针,或停在没有后继的末节点;循环结束后返回空即可。

如果有环,慢指针进入环时,快指针也已经在环内。此后快指针每轮比慢指针多走一步,两者在环上的相对位置每轮改变一格,因此最多再经过一圈的轮数就会相遇。第一次相遇只能说明存在环,相遇点未必是入口。

再定位入口。设从头到入口的距离为 a,入口沿环到相遇点的距离为 b,环长为 c。慢指针相遇时一共走了 t 步,它可能已经多绕了 k 圈,所以有 $t=a+b+kc$;快指针走了 2t 步,二者相遇在同一个节点,路程之差 t 必须是环长的整数倍。消去整圈可知,a + b 也是 c 的整数倍。

把一个新指针 p 放回头节点,让它和相遇点上的 slow 都每轮走一步。走 a 步后,p 刚好到入口;slow 从环上偏移 b 的位置再走 a 步,总偏移 a + b 恰为整圈,也回到入口。

这还是第二阶段的第一次相遇:在走满 a 步之前,p 位于环外,slow 始终位于环内,不可能指向同一节点。如果入口本来就是头节点,a = 0,两个指针在第二阶段开始时就已相同,无需再移动。

快指针每轮要走两步,必须先判断 fast 非空,再判断 fast.next 非空。第一阶段要在移动后比较,不能把两个指针初始都位于头节点误当作有环。空链表、无环单节点和自环也都由这两个阶段自然处理。

解题步骤

  1. 初始化 slow = head、fast = head。
  2. 当 fast 及其后继均非空时,慢指针走一步、快指针走两步,再判断两者是否指向同一节点。
  3. 若相遇,令 p = head,让 p 与 slow 同步每次走一步,直到相同,返回该节点。
  4. 若第一阶段因快指针无法继续前进而退出,返回空。

代码实现

public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;

        // 快指针一次走两步,必须先保证两次访问均有入口。
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;

            if (slow == fast) {
                // 相遇后一个指针回到头节点,再同步走即可找到入环点。
                ListNode p = head;

                while (p != slow) {
                    p = p.next;
                    slow = slow.next;
                }

                return p;
            }
        }

        return null;
    }
}
func detectCycle(head *ListNode) *ListNode {
    // 令一个指针回到头节点,两个指针同步前进,再次相遇点即为入环点。
    slow, fast := head, head

    // 快指针一次走两步,必须先保证两次访问均有入口。
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
        if slow == fast {
            // 相遇后以相同速度前进,头部距离与环内剩余距离在入口对齐。
            p := head
            for p != slow {
                p = p.Next
                slow = slow.Next
            }
            return p
        }
    }

    return nil
}

复杂度分析

  • 时间复杂度:$O(n)$,n 为从头可达的不同节点数。有环时第一阶段至多经过入环距离加一圈的轮数,第二阶段再走入环距离;无环时快指针直接到达末尾。
  • 空间复杂度:$O(1)$,只使用固定数量的节点指针,不记录访问集合,也不修改链表。

关键点总结

[!green]

  • 第一次相遇点通常不是入口。
  • 入口证明依赖一倍与两倍速度对应的整圈关系。

易错点总结

[!yellow]

  • 直接返回第一次相遇点,会返回环内其他节点。
  • 检测阶段漏掉 fast.next 的空判断,会访问空指针。
  • 比较节点值是否相等,会把不同的同值节点误当成相遇;应比较节点引用或指针。
  • 改成三倍速度后仍照搬入口重置步骤,原来的距离关系不再保证成立。

相似题目

题目 难度 关联与区别
141. 环形链表 简单 先用快慢指针判断有环,本题还要根据相遇位置推导并寻找入环点。
287. 寻找重复数 中等 把数组下标跳转视为函数图,可复用找环入口的方法确定重复值。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2024/44966011
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!