【LeetCode Hot100 刷题日记(25/100)】141. 环形链表 —— 链表、双指针、哈希表、Floyd判圈🔄

13 阅读7分钟

📌 题目链接:141. 环形链表 - 力扣(LeetCode)

🔍 难度:简单 | 🏷️ 标签:链表、双指针、哈希表、Floyd判圈

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(1)(最优解)

💡 面试重点:本题是链表中“环检测”的经典问题,常用于考察对「快慢指针」的理解与应用能力。在实际系统中,如内存泄漏检测、循环依赖判断等场景都有其影子。掌握此题能帮助你应对后续更复杂的链表结构问题,例如 142. 环形链表 II。


🧠 一、题目分析

给定一个链表的头节点 head,判断链表中是否存在环。

📌 关键点:

  • 若存在某个节点可以通过 next 指针再次到达,则说明有环。
  • pos 是评测系统内部使用的参数,表示尾部连接到链表中的位置(索引从 0 开始),但不作为输入传递
  • 返回 true 表示有环,否则返回 false

💡 示例解析:

示例1: [3,2,0,-4], pos = 1
        ↓         ↑
       ──────────┘
      3 → 2 → 0 → -4 → 2 (环)

即最后一个节点 -4next 指向了第 1 个节点 2,形成环。

⚠️ 注意:题目不要求我们找到环的入口,只需判断是否存在即可。


🔍 二、核心算法及代码讲解

✅ 方法一:哈希表(Hash Set)

📌 思路

遍历链表时,将每个访问过的节点存入哈希表。如果某次访问到了已存在的节点,说明出现了重复访问,即存在环。

💡 优点

  • 思路直观清晰,容易理解和实现。
  • 时间复杂度低,适合初学者。

❌ 缺点

  • 使用额外空间存储节点地址,空间复杂度为 O(N),不符合“进阶要求”中的 O(1) 空间。

🧩 代码实现

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// Definition for singly-linked list.
struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    bool hasCycle(ListNode* head) {
        unordered_set<ListNode*> seen; // 哈希表记录已访问的节点
        
        while (head != nullptr) {     // 遍历链表直到结尾
            if (seen.count(head)) {   // 如果当前节点已在集合中,说明有环
                return true;
            }
            seen.insert(head);        // 将当前节点加入集合
            head = head->next;        // 移动到下一个节点
        }
        return false;                 // 遍历完无重复,无环
    }
};

📊 复杂度分析

  • 时间复杂度:O(N),最坏情况下需遍历所有节点一次。
  • 空间复杂度:O(N),哈希表最多存储 N 个节点。

✅ 方法二:快慢指针(Floyd 判圈算法)🔥 推荐!

📌 背景知识:Floyd 判圈算法(龟兔赛跑)

🐢 名称来源:“乌龟”代表慢指针,“兔子”代表快指针。

这是由 Robert W. Floyd 提出的经典算法,用于检测单向链表中是否存在环,并可以进一步定位环的入口。

🌀 原理详解

假设链表分为两部分:

  • 非环部分长度a
  • 环的长度b

设慢指针速度为 1,快指针速度为 2。

当它们同时出发时:

  • 慢指针走的距离:d_slow = a + k*b + r (k 是绕环次数)
  • 快指针走的距离:d_fast = 2*(a + k*b + r)

由于快指针比慢指针多走一圈或多圈,最终会追上慢指针(相遇)。

关键结论:只要链表中有环,快慢指针一定会相遇;否则快指针先走到 nullptr

🔧 实现细节

细节解释
slow = head, fast = head->next避免初始相等导致 while 不执行
while(slow != fast)循环条件:未相遇则继续
`if(fast == nullptrfast->next == nullptr)`防止空指针访问

🧩 代码实现

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// Definition for singly-linked list.
struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    bool hasCycle(ListNode* head) {
        // 边界情况:空链表或只有一个节点
        if (head == nullptr || head->next == nullptr) {
            return false;
        }

        ListNode* slow = head;           // 慢指针,每次走一步
        ListNode* fast = head->next;     // 快指针,每次走两步

        while (slow != fast) {           // 只要没有相遇就继续
            if (fast == nullptr || fast->next == nullptr) {
                return false;            // 快指针走到尽头,无环
            }
            slow = slow->next;           // 慢指针前进一步
            fast = fast->next->next;     // 快指针前进两步
        }

        return true;                     // 相遇说明有环
    }
};

📊 复杂度分析

  • 时间复杂度:O(N)
    • 无环时:快指针最多走 2N 步,仍为 O(N)
    • 有环时:相遇最多发生在 N 步内(环长 ≤ N)
  • 空间复杂度:O(1),仅使用两个指针变量。

面试加分项:能够说出这是 Floyd 判圈算法,并解释其原理,是非常加分的表现!


🧭 三、解题思路(分步拆解)

  1. 判断边界情况

    • 如果 head 为空 或 head->next 为空 → 无法成环 → 返回 false
  2. 初始化双指针

    • slow = head
    • fast = head->next
  3. 进入循环

    • slow != fast 时持续移动
    • 每轮:
      • 检查 fast 是否越界(防止空指针访问)
      • slow 向前一步
      • fast 向前两步
  4. 判断结果

    • slow == fast → 说明相遇 → 有环 → 返回 true
    • fast 先走到 nullptr → 无环 → 返回 false
  5. 注意细节

    • 初始位置设置避免死循环
    • 快指针必须检查 fast->next 是否为空

🧪 四、代码测试与验证

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    // 构造测试用例1: [3,2,0,-4], pos=1(-4指向2)
    ListNode* node1 = new ListNode(3);
    ListNode* node2 = new ListNode(2);
    ListNode* node3 = new ListNode(0);
    ListNode* node4 = new ListNode(-4);

    node1->next = node2;
    node2->next = node3;
    node3->next = node4;
    node4->next = node2; // 形成环,指向node2

    Solution sol;
    cout << "Test Case 1 (has cycle): " << (sol.hasCycle(node1) ? "true" : "false") << endl;

    // 构造测试用例2: [1,2], pos=0(2指向1)
    ListNode* node5 = new ListNode(1);
    ListNode* node6 = new ListNode(2);
    node5->next = node6;
    node6->next = node5;

    cout << "Test Case 2 (has cycle): " << (sol.hasCycle(node5) ? "true" : "false") << endl;

    // 构造测试用例3: [1], pos=-1(无环)
    ListNode* node7 = new ListNode(1);
    cout << "Test Case 3 (no cycle): " << (sol.hasCycle(node7) ? "true" : "false") << endl;

    return 0;
}

🔧 输出预期:

Test Case 1 (has cycle): true
Test Case 2 (has cycle): true
Test Case 3 (no cycle): false

🎯 五、面试拓展 & 常见提问

❓ Q1:为什么快慢指针一定能相遇?

因为在环中,快指针每次比慢指针多走一步,相当于在缩小两者之间的距离。只要环存在,距离最终会归零,必然相遇。

❓ Q2:能否用 do-while 替代 while?

可以!但要注意初始值相同的情况。若改为:

do {
    ...
} while (slow != fast);

则需确保初始时 slow != fast,否则跳过循环体。

❓ Q3:如何找到环的入口?(延伸至 142 题)

这是下一题的核心内容。当快慢指针相遇后,再让其中一个指针回到起点,然后两个指针同速前进,再次相遇的位置就是环的入口。

🔗 下一期预告:142. 环形链表 II —— 找环入口!


✅ 六、总结对比表

方法时间复杂度空间复杂度是否推荐适用场景
哈希表O(N)O(N)⚠️ 初学可用易理解,易调试
快慢指针O(N)O(1)✅ 强烈推荐面试必备,效率高

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪


📣 下一期预告:LeetCode 热题 100 第26题——142.环形链表 II(中等)

🔹 题目:给定一个链表,返回环的起始节点。如果无环,返回 nullptr

🔹 核心思路:在快慢指针相遇后,将其中一个指针移回头节点,两个指针以相同速度前进,再次相遇的位置即为环的入口。

🔹 考点:双指针、数学推导、链表环检测进阶。

🔹 难度:中等,是很多图论和系统设计题的基础。

💡 提示:记住公式 a = c - b,其中 a 是头到入口距离,c 是相遇点到入口距离,b 是环长。

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!


快慢指针来追击,
一快一慢不停息。
若有环儿必相遇,
无环终点早结束。

🚀 加油,算法之路,我们一起走到底!