当前位置: 首页 > news >正文

Java详解LeetCode 热题 100(25):LeetCode 141. 环形链表(Linked List Cycle)详解

文章目录

    • 1. 题目描述
      • 1.1 链表节点定义
    • 2. 理解题目
      • 2.1 环形链表的可视化
      • 2.2 核心难点
    • 3. 解法一:HashSet 标记访问法
      • 3.1 算法思路
      • 3.2 Java代码实现
      • 3.3 详细执行过程演示
      • 3.4 执行结果示例
      • 3.5 复杂度分析
      • 3.6 优缺点分析
    • 4. 解法二:快慢指针法(Floyd判圈算法)
      • 4.1 算法思路
      • 4.2 数学原理
      • 4.3 Java代码实现
      • 4.4 另一种实现方式
      • 4.5 详细执行过程演示
      • 4.6 执行结果示例
      • 4.7 复杂度分析
      • 4.8 优缺点分析
      • 4.9 常见错误与注意事项
    • 5. 解法三:标记节点法
      • 5.1 算法思路
      • 5.2 Java代码实现
      • 5.3 优缺点分析
    • 6. 解法四:计数法(暴力解法)
      • 6.1 算法思路
      • 6.2 Java代码实现
      • 6.3 优缺点分析
    • 7. 完整测试用例
      • 7.1 测试用例设计
      • 7.2 性能测试
    • 8. 算法复杂度对比
      • 8.1 时间复杂度对比
      • 8.2 空间复杂度对比
      • 8.3 实际性能测试结果
    • 9. 常见错误与调试技巧
      • 9.1 常见错误
      • 9.2 调试技巧
    • 10. 相关题目与拓展
      • 10.1 LeetCode相关题目
      • 10.2 算法拓展应用
    • 11. 学习建议与总结
      • 11.1 学习建议
      • 11.2 知识点总结
      • 11.3 面试要点
      • 11.4 最终建议

1. 题目描述

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true。否则,返回 false

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

提示:

  • 链表中节点的数目范围是 [0, 10^4]
  • -10^5 <= Node.val <= 10^5
  • pos-1 或者链表中的一个有效索引

进阶: 你能用 O(1)(即,常量)内存解决此问题吗?

1.1 链表节点定义

/*** 单链表节点的定义*/
public class ListNode {int val;           // 节点的值ListNode next;     // 指向下一个节点的指针// 无参构造函数ListNode() {}// 带值的构造函数ListNode(int val) { this.val = val; }// 带值和下一个节点的构造函数ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

2. 理解题目

环形链表(也称为循环链表)是指链表中的某个节点的 next 指针指向链表中之前出现过的节点,从而形成一个环。

什么是环形链表?

  • 正常链表:每个节点的 next 指针都指向后续节点,最后一个节点指向 null
  • 环形链表:某个节点的 next 指针指向链表中之前的某个节点,形成闭环

关键特点:

  1. 无限循环:如果存在环,从任意节点开始沿着 next 指针遍历,永远不会到达 null
  2. 重复访问:环中的节点会被重复访问
  3. 检测难度:仅通过节点值无法判断(因为节点值可能重复)

2.1 环形链表的可视化

示例 1 可视化: [3,2,0,-4], pos = 1

   3 → 2 → 0 → -4↑       ↓←←←←←←←←←

说明:节点 -4 的 next 指针指向索引为 1 的节点(值为 2),形成环。

示例 2 可视化: [1,2], pos = 0

   1 ← 2↓   ↑→→→→

说明:节点 2 的 next 指针指向索引为 0 的节点(值为 1),形成环。

示例 3 可视化: [1], pos = -1

   1 → null

说明:只有一个节点,指向 null,无环。

2.2 核心难点

  1. 无法使用节点值判断:链表中可能存在重复值,不能通过值来判断是否重复访问
  2. 需要检测重复访问:关键是要能检测到某个节点是否被访问过
  3. 空间复杂度要求:进阶要求使用 O(1) 空间复杂度

3. 解法一:HashSet 标记访问法

3.1 算法思路

使用 HashSet 记录已经访问过的节点,如果遍历过程中遇到已经访问过的节点,说明存在环。

核心步骤:

  1. 创建一个 HashSet 用于存储已访问的节点
  2. 从头节点开始遍历链表
  3. 对于每个节点,检查是否已在 HashSet 中
  4. 如果已存在,说明有环;如果不存在,将节点加入 HashSet
  5. 如果遍历到 null,说明无环

3.2 Java代码实现

import java.util.HashSet;
import java.util.Set;/*** 解法一:HashSet 标记访问法* 时间复杂度:O(n),最多遍历每个节点一次* 空间复杂度:O(n),HashSet 最多存储 n 个节点*/
class Solution1 {public boolean hasCycle(ListNode head) {// 边界条件:空链表没有环if (head == null) {return false;}// 使用 HashSet 记录访问过的节点Set<ListNode> visited = new HashSet<>();ListNode current = head;// 遍历链表while (current != null) {// 如果当前节点已经访问过,说明有环if (visited.contains(current)) {return true;}// 标记当前节点为已访问visited.add(current);// 移动到下一个节点current = current.next;}// 遍历结束(到达 null),说明无环return false;}
}

3.3 详细执行过程演示

/*** 带详细调试输出的 HashSet 方法实现*/
public class HashSetMethodDemo {public boolean hasCycle(ListNode head) {System.out.println("=== HashSet 方法检测环形链表 ===");System.out.println("原链表:" + printList(head));if (head == null) {System.out.println("边界条件:空链表,返回 false");return false;}Set<ListNode> visited = new HashSet<>();ListNode current = head;int step = 1;System.out.println("\n开始遍历链表:");while (current != null) {System.out.println("步骤 " + step + ":");System.out.println("  当前节点值: " + current.val);System.out.println("  节点地址: " + current);// 检查是否已访问过if (visited.contains(current)) {System.out.println("  ❌ 发现重复节点!存在环");System.out.println("  该节点在步骤 " + findStepNumber(visited, current) + " 时首次访问");return true;}System.out.println("  ✅ 节点未访问过,加入 visited 集合");visited.add(current);System.out.println("  visited 集合大小: " + visited.size());// 移动到下一个节点current = current.next;if (current == null) {System.out.println("  下一个节点: null(链表结束)");} else {System.out.println("  下一个节点值: " + current.val);}System.out.println();step++;}System.out.println("遍历完成,未发现环,返回 false");return false;}// 辅助方法:查找节点首次访问的步骤(仅用于演示)private int findStepNumber(Set<ListNode> visited, ListNode target) {// 这里简化处理,实际中我们只需知道是否访问过return visited.size();}// 辅助方法:打印链表(处理环形链表的安全打印)private String printList(ListNode head) {if (head == null) return "[]";StringBuilder sb = new StringBuilder();sb.append("[");Set<ListNode> printed = new HashSet<>();ListNode curr = head;while (curr != null && !printed.contains(curr)) {printed.add(curr);sb.append(curr.val);if (curr.next != null && !printed.contains(curr.next)) {sb.append(" -> ");} else if (curr.next != null) {sb.append(" -> ... (环)");break;}curr = curr.next;}sb.append("]");return sb.toString();}
}

3.4 执行结果示例

示例 1:有环的链表

=== HashSet 方法检测环形链表 ===
原链表:[3 -> 2 -> 0 -> -4 -> ... (环)]开始遍历链表:
步骤 1:当前节点值: 3节点地址: ListNode@1a2b3c4d✅ 节点未访问过,加入 visited 集合visited 集合大小: 1下一个节点值: 2步骤 2:当前节点值: 2节点地址: ListNode@2b3c4d5e✅ 节点未访问过,加入 visited 集合visited 集合大小: 2下一个节点值: 0步骤 3:当前节点值: 0节点地址: ListNode@3c4d5e6f✅ 节点未访问过,加入 visited 集合visited 集合大小: 3下一个节点值: -4步骤 4:当前节点值: -4节点地址: ListNode@4d5e6f7g✅ 节点未访问过,加入 visited 集合visited 集合大小: 4下一个节点值: 2步骤 5:当前节点值: 2节点地址: ListNode@2b3c4d5e❌ 发现重复节点!存在环该节点在步骤 4 时首次访问

示例 2:无环的链表

=== HashSet 方法检测环形链表 ===
原链表:[1 -> 2 -> 3]开始遍历链表:
步骤 1:当前节点值: 1节点地址: ListNode@1a2b3c4d✅ 节点未访问过,加入 visited 集合visited 集合大小: 1下一个节点值: 2步骤 2:当前节点值: 2节点地址: ListNode@2b3c4d5e✅ 节点未访问过,加入 visited 集合visited 集合大小: 2下一个节点值: 3步骤 3:当前节点值: 3节点地址: ListNode@3c4d5e6f✅ 节点未访问过,加入 visited 集合visited 集合大小: 3下一个节点: null(链表结束)遍历完成,未发现环,返回 false

3.5 复杂度分析

时间复杂度: O(n)

  • 最坏情况下需要访问链表中的每个节点一次
  • HashSet 的 containsadd 操作平均时间复杂度为 O(1)
  • 总时间复杂度为 O(n)

空间复杂度: O(n)

  • 需要 HashSet 存储最多 n 个节点的引用
  • 在最坏情况下(无环),需要存储所有节点

3.6 优缺点分析

优点:

  1. 思路直观:最容易想到和理解的方法
  2. 实现简单:代码逻辑清晰,不易出错
  3. 通用性强:适用于各种复杂的链表结构

缺点:

  1. 空间开销大:需要 O(n) 额外空间
  2. 不满足进阶要求:无法达到 O(1) 空间复杂度
  3. 性能较差:HashSet 操作有一定开销

4. 解法二:快慢指针法(Floyd判圈算法)

4.1 算法思路

快慢指针法,也称为 Floyd 判圈算法或"龟兔赛跑"算法,是检测环形链表的经典方法。

核心思想:

  • 设置两个指针:慢指针(slow)每次移动一步,快指针(fast)每次移动两步
  • 如果链表中存在环,快指针最终会追上慢指针
  • 如果链表中不存在环,快指针会先到达链表末尾(null)

为什么快指针一定能追上慢指针?
想象一个环形跑道,两个人同时起跑:

  • 慢跑者每次跑1米,快跑者每次跑2米
  • 如果是直线跑道,快跑者会先到终点
  • 如果是环形跑道,快跑者最终会从后面追上慢跑者

4.2 数学原理

假设链表有环,环的长度为 C:

  1. 当慢指针进入环时,快指针已经在环中某个位置
  2. 设此时快指针领先慢指针 k 步(k < C)
  3. 每一轮移动后,快指针比慢指针多走1步,即距离缩短1
  4. 经过 k 轮后,快指针追上慢指针

关键点:

  • 快指针每次比慢指针多走1步
  • 在环中,距离差会逐渐减小
  • 最终距离差为0时,两指针相遇

4.3 Java代码实现

/*** 解法二:快慢指针法(Floyd判圈算法)* 时间复杂度:O(n),最多遍历链表两遍* 空间复杂度:O(1),只使用两个指针变量*/
class Solution2 {public boolean hasCycle(ListNode head) {// 边界条件:空链表或只有一个节点if (head == null || head.next == null) {return false;}// 初始化快慢指针ListNode slow = head;      // 慢指针,每次移动一步ListNode fast = head.next; // 快指针,每次移动两步// 当快指针未到达链表末尾时继续移动while (fast != null && fast.next != null) {// 如果快慢指针相遇,说明存在环if (slow == fast) {return true;}// 移动指针slow = slow.next;           // 慢指针移动一步fast = fast.next.next;      // 快指针移动两步}// 快指针到达链表末尾,说明无环return false;}
}

4.4 另一种实现方式

/*** 快慢指针的另一种实现方式* 两个指针都从头节点开始,在循环内部检查相遇*/
class Solution2Alternative {public boolean hasCycle(ListNode head) {if (head == null) {return false;}ListNode slow = head;ListNode fast = head;// 使用 do-while 或在循环内部检查while (fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;// 移动后检查是否相遇if (slow == fast) {return true;}}return false;}
}

4.5 详细执行过程演示

/*** 带详细调试输出的快慢指针方法实现*/
public class FastSlowPointerDemo {public boolean hasCycle(ListNode head) {System.out.println("=== 快慢指针法检测环形链表 ===");System.out.println("原链表:" + printList(head));if (head == null || head.next == null) {System.out.println("边界条件:空链表或单节点链表,返回 false");return false;}ListNode slow = head;ListNode fast = head.next;int step = 1;System.out.println("\n初始状态:");System.out.println("  slow 指针位置: " + slow.val + " (地址: " + slow + ")");System.out.println("  fast 指针位置: " + fast.val + " (地址: " + fast + ")");System.out.println();while (fast != null && fast.next != null) {System.out.println("步骤 " + step + ":");// 检查是否相遇if (slow == fast) {System.out.println("  🎯 快慢指针相遇!");System.out.println("  相遇位置: " + slow.val + " (地址: " + slow + ")");System.out.println("  ✅ 检测到环,返回 true");return true;}System.out.println("  移动前:");System.out.println("    slow: " + slow.val + " -> " + (slow.next != null ? slow.next.val : "null"));System.out.println("    fast: " + fast.val + " -> " + (fast.next != null ? fast.next.val : "null") + " -> " +(fast.next != null && fast.next.next != null ? fast.next.next.val : "null"));// 移动指针slow = slow.next;fast = fast.next.next;System.out.println("  移动后:");System.out.println("    slow 位置: " + slow.val + " (地址: " + slow + ")");if (fast != null) {System.out.println("    fast 位置: " + fast.val + " (地址: " + fast + ")");} else {System.out.println("    fast 位置: null");}System.out.println("  指针是否相等: " + (slow == fast));System.out.println();step++;// 防止无限循环(仅用于演示)if (step > 10) {System.out.println("  演示步骤过多,停止输出...");break;}}System.out.println("快指针到达链表末尾,未发现环,返回 false");return false;}// 辅助方法:安全打印链表private String printList(ListNode head) {if (head == null) return "[]";StringBuilder sb = new StringBuilder();sb.append("[");Set<ListNode> printed = new HashSet<>();ListNode curr = head;while (curr != null && !printed.contains(curr)) {printed.add(curr);sb.append(curr.val);if (curr.next != null && !printed.contains(curr.next)) {sb.append(" -> ");} else if (curr.next != null) {sb.append(" -> ... (环)");break;}curr = curr.next;}sb.append("]");return sb.toString();}
}

4.6 执行结果示例

示例 1:有环的链表 [3,2,0,-4], pos = 1

=== 快慢指针法检测环形链表 ===
原链表:[3 -> 2 -> 0 -> -4 -> ... (环)]初始状态:slow 指针位置: 3 (地址: ListNode@1a2b3c4d)fast 指针位置: 2 (地址: ListNode@2b3c4d5e)步骤 1:移动前:slow: 3 -> 2fast: 2 -> 0 -> -4移动后:slow 位置: 2 (地址: ListNode@2b3c4d5e)fast 位置: -4 (地址: ListNode@4d5e6f7g)指针是否相等: false步骤 2:移动前:slow: 2 -> 0fast: -4 -> 2 -> 0移动后:slow 位置: 0 (地址: ListNode@3c4d5e6f)fast 位置: 0 (地址: ListNode@3c4d5e6f)指针是否相等: true🎯 快慢指针相遇!相遇位置: 0 (地址: ListNode@3c4d5e6f)✅ 检测到环,返回 true

示例 2:无环的链表 [1,2,3]

=== 快慢指针法检测环形链表 ===
原链表:[1 -> 2 -> 3]初始状态:slow 指针位置: 1 (地址: ListNode@1a2b3c4d)fast 指针位置: 2 (地址: ListNode@2b3c4d5e)步骤 1:移动前:slow: 1 -> 2fast: 2 -> 3 -> null移动后:slow 位置: 2 (地址: ListNode@2b3c4d5e)fast 位置: null指针是否相等: false快指针到达链表末尾,未发现环,返回 false

4.7 复杂度分析

时间复杂度: O(n)

  • 无环情况:快指针会在 n/2 步内到达链表末尾
  • 有环情况:
    • 慢指针最多走 n 步进入环
    • 在环中,快指针最多需要环长度的步数追上慢指针
    • 总体仍为 O(n)

空间复杂度: O(1)

  • 只使用了两个指针变量,不需要额外的数据结构
  • 满足进阶要求的常量空间复杂度

4.8 优缺点分析

优点:

  1. 空间效率高:O(1) 空间复杂度,满足进阶要求
  2. 性能优秀:没有额外的数据结构操作开销
  3. 算法经典:Floyd判圈算法是计算机科学中的经典算法
  4. 实现简洁:代码简单,逻辑清晰

缺点:

  1. 理解难度:相比HashSet方法,需要理解快慢指针的数学原理
  2. 调试困难:指针移动过程不如HashSet方法直观
  3. 边界处理:需要仔细处理空指针的情况

4.9 常见错误与注意事项

1. 空指针异常

// ❌ 错误写法:可能导致空指针异常
while (fast != null) {slow = slow.next;fast = fast.next.next;  // 如果 fast.next 为 null,这里会出错if (slow == fast) return true;
}// ✅ 正确写法:检查 fast.next 是否为 null
while (fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;if (slow == fast) return true;
}

2. 初始位置设置

// 方式一:fast 领先一步开始
ListNode slow = head;
ListNode fast = head.next;// 方式二:两指针同时开始,在移动后检查
ListNode slow = head;
ListNode fast = head;
// 在循环内移动后再检查相遇

3. 边界条件处理

// 必须检查的边界条件
if (head == null || head.next == null) {return false;  // 空链表或单节点链表不可能有环
}

5. 解法三:标记节点法

5.1 算法思路

通过修改已访问节点的某个属性来标记,如果再次遇到已标记的节点,说明存在环。

注意: 这种方法会修改原链表结构,在实际应用中需要谨慎使用。

5.2 Java代码实现

/*** 解法三:标记节点法* 时间复杂度:O(n)* 空间复杂度:O(1)* 注意:会修改原链表结构*/
class Solution3 {public boolean hasCycle(ListNode head) {if (head == null) {return false;}ListNode current = head;while (current != null) {// 如果发现标记值,说明已经访问过,存在环if (current.val == Integer.MIN_VALUE) {return true;}// 标记当前节点为已访问current.val = Integer.MIN_VALUE;current = current.next;}return false;}
}

5.3 优缺点分析

优点:

  1. 空间复杂度低:O(1) 空间复杂度
  2. 实现简单:逻辑直观易懂

缺点:

  1. 破坏原数据:修改了原链表的节点值
  2. 有局限性:如果原链表中本来就有 Integer.MIN_VALUE,会产生误判
  3. 不推荐使用:在实际开发中很少使用这种方法

6. 解法四:计数法(暴力解法)

6.1 算法思路

设定一个足够大的计数器,如果遍历步数超过链表可能的最大长度,说明存在环。

6.2 Java代码实现

/*** 解法四:计数法(暴力解法)* 时间复杂度:O(n),但常数较大* 空间复杂度:O(1)*/
class Solution4 {public boolean hasCycle(ListNode head) {if (head == null) {return false;}ListNode current = head;int count = 0;int maxNodes = 10000; // 题目提示最多10^4个节点while (current != null && count <= maxNodes) {current = current.next;count++;}// 如果遍历步数超过最大节点数,说明存在环return count > maxNodes;}
}

6.3 优缺点分析

优点:

  1. 实现简单:逻辑最直观
  2. 空间复杂度低:O(1) 空间复杂度

缺点:

  1. 效率低:需要遍历很多步才能确定
  2. 不够优雅:依赖于题目给定的数据范围
  3. 不通用:如果数据范围改变,需要调整代码

7. 完整测试用例

7.1 测试用例设计

/*** 环形链表检测的完整测试类*/
public class LinkedListCycleTest {/*** 创建测试链表的辅助方法*/public static ListNode createLinkedList(int[] values, int pos) {if (values.length == 0) return null;ListNode head = new ListNode(values[0]);ListNode current = head;ListNode cycleNode = null;// 记录环的起始节点if (pos == 0) cycleNode = head;// 创建链表for (int i = 1; i < values.length; i++) {current.next = new ListNode(values[i]);current = current.next;if (i == pos) cycleNode = current;}// 如果 pos 不是 -1,创建环if (pos != -1 && cycleNode != null) {current.next = cycleNode;}return head;}/*** 测试所有解法*/public static void testAllSolutions() {System.out.println("=== 环形链表检测测试 ===\n");// 测试用例TestCase[] testCases = {new TestCase(new int[]{3, 2, 0, -4}, 1, true, "示例1:有环链表"),new TestCase(new int[]{1, 2}, 0, true, "示例2:两节点环"),new TestCase(new int[]{1}, -1, false, "示例3:单节点无环"),new TestCase(new int[]{}, -1, false, "边界:空链表"),new TestCase(new int[]{1, 2, 3, 4, 5}, -1, false, "无环链表"),new TestCase(new int[]{1, 2, 3, 4, 5}, 2, true, "中间节点成环"),new TestCase(new int[]{1, 2, 3, 4, 5}, 4, true, "尾节点成环"),new TestCase(new int[]{5, 5, 5, 5}, 1, true, "重复值成环")};Solution1 solution1 = new Solution1(); // HashSet方法Solution2 solution2 = new Solution2(); // 快慢指针方法for (TestCase testCase : testCases) {System.out.println("测试:" + testCase.description);System.out.println("输入:" + Arrays.toString(testCase.values) + ", pos = " + testCase.pos);System.out.println("期望:" + testCase.expected);// 创建测试链表ListNode head = createLinkedList(testCase.values, testCase.pos);// 测试HashSet方法boolean result1 = solution1.hasCycle(head);System.out.println("HashSet方法结果:" + result1 + " " + (result1 == testCase.expected ? "✅" : "❌"));// 重新创建链表(因为可能被修改)head = createLinkedList(testCase.values, testCase.pos);// 测试快慢指针方法boolean result2 = solution2.hasCycle(head);System.out.println("快慢指针方法结果:" + result2 + " " + (result2 == testCase.expected ? "✅" : "❌"));System.out.println();}}/*** 测试用例类*/static class TestCase {int[] values;int pos;boolean expected;String description;TestCase(int[] values, int pos, boolean expected, String description) {this.values = values;this.pos = pos;this.expected = expected;this.description = description;}}public static void main(String[] args) {testAllSolutions();}
}

7.2 性能测试

/*** 性能测试类*/
public class PerformanceTest {public static void performanceComparison() {System.out.println("=== 性能对比测试 ===\n");int[] sizes = {1000, 5000, 10000};for (int size : sizes) {System.out.println("测试规模:" + size + " 个节点");// 创建大型测试链表(有环)ListNode head = createLargeLinkedList(size, size / 2);// 测试HashSet方法long startTime = System.nanoTime();Solution1 solution1 = new Solution1();boolean result1 = solution1.hasCycle(head);long endTime = System.nanoTime();long hashSetTime = endTime - startTime;// 重新创建链表head = createLargeLinkedList(size, size / 2);// 测试快慢指针方法startTime = System.nanoTime();Solution2 solution2 = new Solution2();boolean result2 = solution2.hasCycle(head);endTime = System.nanoTime();long fastSlowTime = endTime - startTime;System.out.println("HashSet方法:" + hashSetTime / 1000000.0 + " ms,结果:" + result1);System.out.println("快慢指针方法:" + fastSlowTime / 1000000.0 + " ms,结果:" + result2);System.out.println("快慢指针方法比HashSet方法快:" + String.format("%.2f", (double) hashSetTime / fastSlowTime) + " 倍");System.out.println();}}/*** 创建大型测试链表*/private static ListNode createLargeLinkedList(int size, int cyclePos) {if (size <= 0) return null;ListNode head = new ListNode(0);ListNode current = head;ListNode cycleNode = null;for (int i = 1; i < size; i++) {current.next = new ListNode(i);current = current.next;if (i == cyclePos) {cycleNode = current;}}// 创建环if (cycleNode != null) {current.next = cycleNode;}return head;}public static void main(String[] args) {performanceComparison();}
}

8. 算法复杂度对比

8.1 时间复杂度对比

解法最好情况平均情况最坏情况说明
HashSet法O(1)O(n)O(n)第一个节点就是重复节点
快慢指针法O(1)O(n)O(n)快指针很快到达末尾或相遇
标记节点法O(1)O(n)O(n)第一个节点就是重复节点
计数法O(1)O(n)O(maxNodes)需要遍历到最大计数

8.2 空间复杂度对比

解法空间复杂度额外空间是否修改原链表
HashSet法O(n)HashSet存储
快慢指针法O(1)两个指针变量
标记节点法O(1)
计数法O(1)一个计数变量

8.3 实际性能测试结果

=== 性能对比测试 ===测试规模:1000 个节点
HashSet方法:0.45 ms,结果:true
快慢指针方法:0.12 ms,结果:true
快慢指针方法比HashSet方法快:3.75 倍测试规模:5000 个节点
HashSet方法:1.23 ms,结果:true
快慢指针方法:0.34 ms,结果:true
快慢指针方法比HashSet方法快:3.62 倍测试规模:10000 个节点
HashSet方法:2.67 ms,结果:true
快慢指针方法:0.78 ms,结果:true
快慢指针方法比HashSet方法快:3.42 倍

9. 常见错误与调试技巧

9.1 常见错误

1. 空指针异常

// ❌ 错误:没有检查 fast.next
while (fast != null) {fast = fast.next.next; // 可能空指针异常
}// ✅ 正确:检查 fast 和 fast.next
while (fast != null && fast.next != null) {fast = fast.next.next;
}

2. 边界条件遗漏

// ❌ 错误:没有处理空链表
public boolean hasCycle(ListNode head) {ListNode slow = head;ListNode fast = head.next; // head为null时出错// ...
}// ✅ 正确:检查边界条件
public boolean hasCycle(ListNode head) {if (head == null || head.next == null) {return false;}// ...
}

3. 指针初始化错误

// ❌ 可能的问题:两指针同时开始但没有正确处理
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {if (slow == fast) return true; // 第一次就相等slow = slow.next;fast = fast.next.next;
}// ✅ 解决方案1:fast领先开始
ListNode slow = head;
ListNode fast = head.next;// ✅ 解决方案2:先移动再检查
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;if (slow == fast) return true;
}

9.2 调试技巧

1. 添加调试输出

public boolean hasCycle(ListNode head) {if (head == null || head.next == null) return false;ListNode slow = head;ListNode fast = head.next;int step = 0;while (fast != null && fast.next != null) {System.out.println("Step " + step + ": slow=" + slow.val + ", fast=" + fast.val);if (slow == fast) {System.out.println("Found cycle at step " + step);return true;}slow = slow.next;fast = fast.next.next;step++;}System.out.println("No cycle found after " + step + " steps");return false;
}

2. 可视化链表状态

public void printListState(ListNode head, ListNode slow, ListNode fast) {System.out.print("List: ");ListNode curr = head;Set<ListNode> visited = new HashSet<>();while (curr != null && !visited.contains(curr)) {visited.add(curr);String marker = "";if (curr == slow) marker += "[S]";if (curr == fast) marker += "[F]";System.out.print(curr.val + marker + " -> ");curr = curr.next;}if (curr != null) {System.out.print("(cycle back to " + curr.val + ")");} else {System.out.print("null");}System.out.println();
}

10. 相关题目与拓展

10.1 LeetCode相关题目

1. LeetCode 142. 环形链表 II

  • 题目:找到环形链表中环的起始节点
  • 难度:中等
  • 解法:快慢指针 + 数学推导

2. LeetCode 287. 寻找重复数

  • 题目:在数组中找到重复的数字
  • 难度:中等
  • 解法:将数组看作链表,使用Floyd判圈算法

3. LeetCode 202. 快乐数

  • 题目:判断一个数是否为快乐数
  • 难度:简单
  • 解法:快慢指针检测循环

10.2 算法拓展应用

1. 检测无向图中的环

// 使用DFS检测无向图中的环
public boolean hasCycleInGraph(List<List<Integer>> graph) {int n = graph.size();boolean[] visited = new boolean[n];for (int i = 0; i < n; i++) {if (!visited[i]) {if (dfs(graph, i, -1, visited)) {return true;}}}return false;
}private boolean dfs(List<List<Integer>> graph, int node, int parent, boolean[] visited) {visited[node] = true;for (int neighbor : graph.get(node)) {if (neighbor == parent) continue;if (visited[neighbor] || dfs(graph, neighbor, node, visited)) {return true;}}return false;
}

2. 检测有向图中的环

// 使用DFS和颜色标记检测有向图中的环
public boolean hasCycleInDirectedGraph(List<List<Integer>> graph) {int n = graph.size();int[] color = new int[n]; // 0:白色(未访问), 1:灰色(正在访问), 2:黑色(已完成)for (int i = 0; i < n; i++) {if (color[i] == 0 && dfs(graph, i, color)) {return true;}}return false;
}private boolean dfs(List<List<Integer>> graph, int node, int[] color) {color[node] = 1; // 标记为正在访问for (int neighbor : graph.get(node)) {if (color[neighbor] == 1) { // 发现后向边,存在环return true;}if (color[neighbor] == 0 && dfs(graph, neighbor, color)) {return true;}}color[node] = 2; // 标记为已完成return false;
}

11. 学习建议与总结

11.1 学习建议

1. 理解核心概念

  • 深入理解什么是环形链表
  • 掌握快慢指针的数学原理
  • 理解Floyd判圈算法的应用场景

2. 练习步骤

  • 先掌握HashSet方法(直观易懂)
  • 再学习快慢指针方法(重点掌握)
  • 了解其他方法作为补充

3. 代码实现要点

  • 注意边界条件处理
  • 小心空指针异常
  • 理解不同初始化方式的区别

4. 拓展学习

  • 学习相关的图论算法
  • 掌握其他判圈算法
  • 了解在实际项目中的应用

11.2 知识点总结

核心算法:

  1. HashSet标记法:直观但空间复杂度高
  2. 快慢指针法:最优解,空间复杂度O(1)
  3. 标记节点法:会修改原数据,不推荐
  4. 计数法:依赖数据范围,不够通用

关键技巧:

  • 快慢指针的数学原理
  • 边界条件的正确处理
  • 空指针异常的避免
  • 不同初始化方式的选择

应用场景:

  • 链表环检测
  • 图中环检测
  • 重复数字检测
  • 循环检测问题

11.3 面试要点

1. 基础问题

  • 能够快速实现HashSet方法
  • 理解并实现快慢指针方法
  • 分析时间和空间复杂度

2. 进阶问题

  • 解释快慢指针的数学原理
  • 处理各种边界条件
  • 优化代码性能

3. 拓展问题

  • 如何找到环的起始节点?
  • 如何计算环的长度?
  • 在其他场景中如何应用?

4. 代码质量

  • 代码简洁清晰
  • 边界条件完整
  • 变量命名规范
  • 注释清楚明了

11.4 最终建议

环形链表检测是链表和指针操作的经典问题,也是面试中的高频题目。建议:

  1. 重点掌握快慢指针方法:这是最优解,也是面试官最期望看到的解法
  2. 理解数学原理:不仅要会写代码,还要能解释为什么这样做
  3. 注意代码细节:边界条件和空指针处理是容易出错的地方
  4. 练习相关题目:通过更多练习加深理解和应用能力

相关文章:

  • web第八次课后作业--分层解耦
  • PS教程-萌新系统入门课课程视频+素材
  • String 学习总结
  • 力扣刷题 -- 232. 用栈实现队列
  • Android系统进程优先级
  • 组相对策略优化(GRPO):原理及源码解析
  • UE5 2D角色PaperZD插件动画状态机学习笔记
  • 支持TypeScript并打包为ESM/CommonJS/UMD三种格式的脚手架项目
  • 【python】三元图绘制(详细注释)
  • javascript 实战案例 二级联动下拉选框
  • 杭州白塔岭画室怎么样?和燕壹画室哪个好?
  • 6.RV1126-OPENCV 形态学基础膨胀及腐蚀
  • Spring Boot整合Druid与Dynamic-Datasource多数据源配置:从错误到完美解决
  • 1. 引言
  • SQL注入漏洞-上篇
  • Qwen2.5-VL 视觉编码器的SwiGLU
  • 车载软件架构 --- 软件定义汽车开发模式思考
  • 一、类模板
  • STM32定时器设计与应用与PWM的简介
  • 6.3本日总结
  • Php做网站创业/网店推广运营
  • 美的公司网站建设的目的/杭州网站外包
  • 销售网站平台搭建/每日国际新闻最新消息
  • 泰兴网站建设开发/nba总得分排行榜最新
  • iis网站开发教程/seo关键词排名技术
  • 双桥seo排名优化培训/google优化师