快慢指针妙解链表环问题

算法概念

1、定义

        判断单链表是否有环的快慢指针法(也叫龟兔赛跑算法),是效率最优的方案(时间 O (n)、空间 O (1)),核心逻辑是:通过两个速度不同的指针遍历链表,若链表有环,快指针终将追上慢指针;若无环,快指针会先走到链表末尾(指向 null)。

slow = slow->next;
fast = fast->next->next;
public class LinkedListCycle {
    public boolean hasCycle(ListNode head) {
        // 边界处理:空链表或只有一个节点,无环
        if (head == null || head->next == null) {
            return false;
        }
        
        // 初始化快慢指针(快指针先多走1步,避免初始状态相等)
        ListNode slow = head;
        ListNode fast = head->next; // 优化:初始错开,减少循环次数
        
        // 循环条件:快指针未走到末尾
        while (fast != null && fast->next != null) {
            if (slow == fast) { // 相遇,有环
                return true;
            }
            slow = slow->next; // 慢指针走1步
            fast = fast->next->next; // 快指针走2步
        }
        
        // 快指针走到末尾,无环
        return false;
    }
}

2、常见疑问

① 快指针为什么在有环的情况下会追上慢指针?(为什么不会发生快指针直接超过慢指针的情况?)

答:当慢指针进入环后,快慢指针就处于同一个环形空间,两者之间的距离是 “环形距离”(范围:0 ~ 环长 L-1)。由于每轮速度差 1 步,这个环形距离只会每轮缩小 1 步(不是 “随机变化” 或 “跳跃式变化”)。

注意:快慢指针的速度之差只能在1步(如果环的大小 ≥ 快慢指针步数之差,会发生快指针直接超过慢指针的情况)。

② while()循环的条件怎么来的?(如果没有环,该怎么退出循环?)

答:

fast == null:快指针刚好到表尾

fast->next == null:快指针的下一个结点就是表尾,但是因为快指针一次性走两步,所以会访问fast->next->next但是fast->next已经是空指针了,会发生空指针访问。

3、进阶——怎么找到入口点(相遇点就是入口点?)

答:第一次相遇点不一定就是入口点!

让AI帮我解释一下,偷个懒@^_^@

推导

先定义 3 个核心变量,让推导更清晰:

  • 设:链表头到环入口的距离为 a(步数);
  • 设:环的长度为 L(步数,即环内节点总数);
  • 设:快慢指针第一次相遇时,慢指针总共走了 k 步。
步骤 1:基于速度差推导 k 与 L 的关系
  • 快指针速度是 2 步 / 轮,慢指针是 1 步 / 轮,所以相遇时快指针走了 2k 步;
  • 快慢指针的步数差:2k - k = k 步;
  • 由于相遇时快指针已经在环内循环了至少 1 圈(否则追不上慢指针),所以步数差 k 一定是环长 L 的整数倍(比如 1 倍、2 倍...),即:k = n×Ln 是正整数,代表快指针在环内转了 n 圈)。
步骤 2:推导 a 与相遇点的关系
  • 慢指针走的 k 步,可拆分为两部分:从表头到环入口的 a 步 + 从环入口到相遇点的 b 步(b < L,因为还没绕环 1 圈);
  • 所以:k = a + b
  • 结合步骤 1 的结论 k = n×L,可得:a + b = n×L → 变形为:a = n×L - b
  • 而 n×L - b 的物理意义是:从相遇点出发,绕环走 n×L - b 步,恰好能到达环入口(因为 b 是相遇点到入口的步数,L - b 是入口到相遇点的反向步数,n×L 是绕环 n 圈,不影响最终位置)。
步骤 3:为什么 “重置 + 同步移动” 能找到入口?
  • 由 a = n×L - b 可知:从表头到入口的距离 a,等于从相遇点到入口的环形距离(n×L - b
  • 此时若将慢指针重置为头节点,快指针留在相遇点,然后两者同时以 “1 步 / 轮” 移动:
    • 慢指针从表头走 a 步,会到达环入口;
    • 快指针从相遇点走 a 步(即 n×L - b 步),也会到达环入口;
  • 因此,两者会在环入口点再次相遇 —— 这就是找到入口的核心逻辑!

通俗示例

假设:a=3(头到入口 3 步),L=5(环长 5),n=1(快指针绕环 1 圈)。

  • 慢指针走 k = n×L = 5 步(相遇时):5 = 3(a) + 2(b) → 相遇点在环内距离入口 2 步的位置;
  • 变形公式:a = 1×5 - 2 = 3 → 从相遇点走 3 步到入口;
  • 操作:慢指针重置到表头,走 3 步到入口;快指针从相遇点走 3 步(2 步到入口 + 1 步绕环,因 n=1),两者同时到达入口,相遇。
// 找到环的入口节点(无环返回nullptr)
ListNode* detectCycle(ListNode* head) {
    if (head == nullptr || head->next == nullptr) {
        return nullptr;
    }

    ListNode* slow = head;
    ListNode* fast = head;
    bool hasCycle = false;

    // 第一步:找第一次相遇点(确认有环)
    while (fast != nullptr && fast->next != nullptr) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) { // 第一次相遇,有环
            hasCycle = true;
            break;
        }
    }

    // 无环则返回null
    if (!hasCycle) {
        return nullptr;
    }

    // 第二步:重置慢指针,同步移动找入口
    slow = head;
    while (slow != fast) {
        slow = slow->next;
        fast = fast->next; // 现在快指针也走1步
    }

    return slow; // 再次相遇,即为入口
}

力扣例题

141. 环形链表 - 力扣(LeetCode)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool hasCycle(ListNode* head) {
        if (head == NULL || head->next == NULL)
            return false;
        auto slow = head;
        auto fast = head->next;
        while (fast != NULL && fast->next != NULL) {
            if (fast == slow)
                return true;
            slow = slow->next;
            fast = fast->next->next;
        }
        return false;
    }
};

思考:快慢指针还能干嘛?(怎么快速找到一个链表的中间结点?)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值