【算法--链表】142.环形链表中Ⅱ--通俗讲解如何找链表中环的起点

93 阅读6分钟

一、题目是啥?一句话说清

给定一个链表,如果链表有环,返回环的起始节点;如果无环,返回 null。不能修改链表。

二、解题核心

使用快慢指针法:先找到快慢指针的相遇点,然后让一个指针从头开始,另一个从相遇点开始,以相同速度移动,它们再次相遇的点就是环的入口。

这就像两个人在环形跑道上跑步:

  1. 快的人速度是慢的人的两倍,他们最终会相遇。
  2. 相遇后,让快的人回到起点,然后两人以相同速度跑步,他们再次相遇的地方就是跑道的入口。

三、关键在哪里?(3个核心点)

想理解并解决这道题,必须抓住以下三个关键点:

1. 快慢指针的相遇点

  • 是什么:快指针每次走两步,慢指针每次走一步,如果存在环,它们一定会相遇。
  • 为什么重要:相遇点是我们找到环入口的起点。

2. 数学关系推导

  • 是什么:从头节点到环入口的距离 = 从相遇点到环入口的距离 + n圈环长。
  • 为什么重要:这个数学关系保证了从头节点和相遇点同时出发的两个指针,会在环入口相遇。

3. 第二次移动的同步速度

  • 是什么:找到相遇点后,两个指针都以每次一步的速度移动。
  • 为什么重要:这样能确保它们正好在环入口相遇,而不是错过。

四、看图理解流程(通俗理解版本)

假设链表:1 → 2 → 3 → 4 → 5 → 3(形成环,5指向3)

  1. 第一階段:找到相遇点

    • 慢指针每次走一步,快指针每次走两步。
    • 慢指针路径:1 → 2 → 3 → 4 → 5 → 3 ...
    • 快指针路径:1 → 3 → 5 → 4 → 3 → 5 ...
    • 它们在节点4相遇(示例中可能相遇在其他点,但总会相遇)。
  2. 第二階段:找到环入口

    • 将快指针重新指向头节点(节点1)。
    • 快指针和慢指针都每次走一步:
      • 快指针从1开始:1 → 2 → 3
      • 慢指针从4开始:4 → 5 → 3
    • 它们在节点3相遇,节点3就是环的入口。

五、C++ 代码实现(附详细注释)

#include <iostream>
using namespace std;

// 链表节点定义
struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        // 处理空链表或单节点链表的情况
        if (head == nullptr || head->next == nullptr) {
            return nullptr;
        }
        
        // 初始化快慢指针
        ListNode *slow = head;
        ListNode *fast = head;
        
        // 第一階段:找到相遇点
        while (fast != nullptr && fast->next != nullptr) {
            slow = slow->next;         // 慢指针移动一步
            fast = fast->next->next;   // 快指针移动两步
            
            if (slow == fast) {
                // 找到相遇点,开始第二階段
                break;
            }
        }
        
        // 如果快指针遇到null,说明无环
        if (fast == nullptr || fast->next == nullptr) {
            return nullptr;
        }
        
        // 第二階段:找到环入口
        fast = head; // 将快指针重新指向头节点
        while (fast != slow) {
            fast = fast->next; // 两者都每次移动一步
            slow = slow->next;
        }
        
        return fast; // 相遇点就是环入口
    }
};

// 辅助函数:创建带环的链表用于测试
ListNode* createCycleList() {
    ListNode* node1 = new ListNode(1);
    ListNode* node2 = new ListNode(2);
    ListNode* node3 = new ListNode(3);
    ListNode* node4 = new ListNode(4);
    ListNode* node5 = new ListNode(5);
    
    node1->next = node2;
    node2->next = node3;
    node3->next = node4;
    node4->next = node5;
    node5->next = node3; // 形成环:5指向3
    
    return node1;
}

// 测试代码
int main() {
    Solution solution;
    
    // 测试有环的情况
    ListNode* cycleList = createCycleList();
    ListNode* entry = solution.detectCycle(cycleList);
    if (entry != nullptr) {
        cout << "环的入口节点值: " << entry->val << endl;
    } else {
        cout << "无环" << endl;
    }
    
    // 注意:实际使用时需要释放内存,这里简化示例
    return 0;
}

六、注意事项

  • 空指针检查:在移动指针时,始终检查指针是否为null,避免空指针异常。
  • 无环处理:如果快指针遇到null,说明无环,直接返回null。
  • 数学理解:理解为什么第二次移动能找到环入口是关键,但不要求严格证明,掌握直觉即可。
  • 内存管理:在实际应用中,如果需要创建测试链表,记得释放内存,但面试中通常更关注算法逻辑。

七、总结

理解此题的关键在于:

  • 快慢指针相遇:首先确认链表有环,并找到相遇点。
  • 重新出发:让一个指针从头开始,另一个从相遇点开始,以相同速度移动。
  • 相遇即入口:它们再次相遇的点就是环的入口。

掌握这三点,你就能高效解决环形链表入口问题。这道题是快慢指针法的进阶应用,也是面试中常见的高频题目。多练习几次,理解其中的数学直觉,就能熟练运用。


数学原理(为什么第二次相遇就是入口?)

这是最关键的部分,我们来解释下。

我们定义几个点:

  • A: 链表起点。
  • B: 环的入口(也是我们想找的点)。
  • C: 快指针和慢指针第一次相遇的点。

再定义几个距离:

  • AB = a:从起点到环入口的距离。
  • BC = b:从环入口到第一次相遇点的距离。
  • 环的周长 = c

第一次相遇时,它们跑了多少?

  • 慢指针跑的路程S_slow = a + b

    • (从A到B是a,再从B到C是b)
  • 快指针跑的路程S_fast = a + b + n * c

    • 它已经跑完了a+b,并且还在环里额外绕了n圈(n是大于等于1的整数)。

关键关系:因为快指针速度是慢指针的两倍,所以在相同时间内,快指针跑的路程是慢指针的两倍。 S_fast = 2 * S_slow

代入公式: a + b + n * c = 2 * (a + b)

现在我们解这个方程: a + b + n * c = 2a + 2b n * c = a + b a = n * c - b

这个 a = n * c - b 就是整个问题的灵魂!

它意味着什么?

  • n * c - b 可以理解为:从相遇点C出发,走n*c - b步,会到达哪里?
  • n*c 是绕环n圈,等于没动,还是在C点。
  • -b 的意思是 从C点倒退回b步
    • C点退回b步,正好就回到了环的入口B
    • (BC = b,所以 CB 就是 -b)

所以,a = n * c - b 意味着:从起点A到入口B的距离a,等于从相遇点C倒退b步的距离。

现在再看我们第二阶段的操作:

  1. 快指针被放回起点A,它离入口B的距离正好是a
  2. 慢指针留在相遇点C,它离入口B的距离是 c - b(因为从C点再走c-b步也能绕回B点,但注意我们的公式是 a = n*c -b,对于慢指针来说,n=1时,它离入口的距离就是 c - b)。

让它们现在都以每次一步的速度前进:

  • 快指针从A点出发,走a步后,恰好到达B点(环的入口)。
  • 慢指针从C点出发,走多少步能到B点呢?它需要走 c - b 步。
    • 但是看我们的公式 a = n*c - b。如果 n=1,那么 a = c - b。慢指针走 a(即 c-b)步正好也到B点。
    • 如果 n>1,比如 n=2a = 2c - b。快指针需要走 a(即 2c-b) 步到B点。慢指针从C点走 2c - b 步,相当于在环里绕了2圈再倒退b步,最终停留的位置依然是B点!

结论:无论n是多少,快指针和慢指针以相同速度同时出发,它们都将在走a步后,在环的入口点B相遇!