Java详解LeetCode 热题 100(26):LeetCode 142. 环形链表 II(Linked List Cycle II)详解

news2025/6/10 16:49:34

1. 题目描述

给定一个链表,返回链表开始入环的第一个节点。如果链表无环,则返回 null

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

不允许修改链表。

示例 1:

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

示例 2:

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

示例 3:

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

提示:

  • 链表中节点的数目范围在范围 [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. 理解题目

环形链表 II 是环形链表问题的进阶版本。在环形链表 I 中,我们只需要判断链表是否有环;而在环形链表 II 中,我们需要找到环的起始节点。

关键概念:

  1. 环的起始节点:第一个被重复访问的节点,也就是环的入口
  2. 环的检测:首先需要确定链表中是否存在环
  3. 环的定位:在确定有环的前提下,找到环的起始位置

2.1 问题可视化

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

   3 → 2 → 0 → -4
       ↑       ↓
       ←←←←←←←←←
       环的起始节点

说明:节点 2(索引为 1)是环的起始节点。

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

   1 ← 2
   ↑   ↓
   →→→→
   环的起始节点

说明:节点 1(索引为 0)是环的起始节点。

2.2 核心挑战

  1. 两阶段问题:首先检测环,然后定位环的起始节点
  2. 数学推导:需要理解快慢指针相遇后的数学关系
  3. 空间复杂度要求:进阶要求使用 O(1) 空间复杂度

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

3.1 算法思路

使用 HashSet 记录已经访问过的节点,第一个重复访问的节点就是环的起始节点。

核心步骤:

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

3.2 Java代码实现

import java.util.HashSet;
import java.util.Set;

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

3.3 详细执行过程演示

/**
 * 带详细调试输出的 HashSet 方法实现
 */
public class HashSetMethodDemo {
    public ListNode detectCycle(ListNode head) {
        System.out.println("=== HashSet 方法检测环形链表起始节点 ===");
        System.out.println("原链表:" + printList(head));
        
        if (head == null) {
            System.out.println("边界条件:空链表,返回 null");
            return null;
        }
        
        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("  环的起始节点值: " + current.val);
                System.out.println("  环的起始节点地址: " + current);
                return current;
            }
            
            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("遍历完成,未发现环,返回 null");
        return null;
    }
    
    // 辅助方法:安全打印链表
    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(" -> ... (环起始于节点 " + curr.next.val + ")");
                break;
            }
            curr = curr.next;
        }
        sb.append("]");
        return sb.toString();
    }
}

3.4 执行结果示例

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

=== HashSet 方法检测环形链表起始节点 ===
原链表:[3 -> 2 -> 0 -> -4 -> ... (环起始于节点 2)]

开始遍历链表:
步骤 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
  🎯 发现重复节点!这是环的起始节点
  环的起始节点值: 2
  环的起始节点地址: ListNode@2b3c4d5e

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 快慢指针法是解决环形链表问题的经典算法,分为两个阶段:

第一阶段:检测环的存在

  • 使用快慢指针检测链表中是否存在环
  • 如果存在环,快慢指针会在环中某点相遇

第二阶段:定位环的起始节点

  • 利用数学关系,通过特定的指针移动策略找到环的起始节点

4.2 数学原理推导

这是理解 Floyd 算法的关键部分。让我们详细推导数学关系:

设定变量:

  • a:从链表头到环起始节点的距离
  • b:从环起始节点到快慢指针相遇点的距离
  • c:从相遇点回到环起始节点的距离
  • 环的长度 = b + c

第一阶段分析:
当快慢指针第一次相遇时:

  • 慢指针走过的距离:a + b
  • 快指针走过的距离:a + b + n(b + c)(n 为快指针在环中多走的圈数)

由于快指针速度是慢指针的 2 倍:

2(a + b) = a + b + n(b + c)
2a + 2b = a + b + n(b + c)
a + b = n(b + c)
a = n(b + c) - b
a = (n-1)(b + c) + c

关键结论:
当 n = 1 时(快指针只比慢指针多走一圈),有:a = c

这意味着:从链表头到环起始节点的距离 = 从相遇点到环起始节点的距离

4.3 算法步骤详解

/**
 * 解法二:Floyd 快慢指针法
 * 时间复杂度:O(n)
 * 空间复杂度:O(1)
 */
class Solution2 {
    public ListNode detectCycle(ListNode head) {
        // 边界条件检查
        if (head == null || head.next == null) {
            return null;
        }
        
        // 第一阶段:检测环的存在
        ListNode slow = head;
        ListNode fast = head;
        
        // 快慢指针移动,检测是否有环
        while (fast != null && fast.next != null) {
            slow = slow.next;           // 慢指针移动一步
            fast = fast.next.next;      // 快指针移动两步
            
            // 如果快慢指针相遇,说明存在环
            if (slow == fast) {
                break;
            }
        }
        
        // 如果没有环,返回 null
        if (fast == null || fast.next == null) {
            return null;
        }
        
        // 第二阶段:定位环的起始节点
        // 将一个指针重置到头节点
        ListNode ptr1 = head;
        ListNode ptr2 = slow;  // 从相遇点开始
        
        // 两个指针以相同速度移动,相遇点就是环的起始节点
        while (ptr1 != ptr2) {
            ptr1 = ptr1.next;
            ptr2 = ptr2.next;
        }
        
        return ptr1;  // 返回环的起始节点
    }
}

4.4 详细执行过程演示

/**
 * 带详细调试输出的 Floyd 方法实现
 */
public class FloydMethodDemo {
    public ListNode detectCycle(ListNode head) {
        System.out.println("=== Floyd 快慢指针法检测环形链表起始节点 ===");
        System.out.println("原链表:" + printList(head));
        
        if (head == null || head.next == null) {
            System.out.println("边界条件:空链表或单节点链表,返回 null");
            return null;
        }
        
        // 第一阶段:检测环
        System.out.println("\n=== 第一阶段:检测环的存在 ===");
        ListNode slow = head;
        ListNode fast = head;
        int step = 0;
        
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            step++;
            
            System.out.println("步骤 " + step + ":");
            System.out.println("  slow 位置: " + slow.val + " (地址: " + slow + ")");
            System.out.println("  fast 位置: " + fast.val + " (地址: " + fast + ")");
            
            if (slow == fast) {
                System.out.println("  🎯 快慢指针相遇!检测到环");
                System.out.println("  相遇位置: " + slow.val);
                break;
            }
            System.out.println("  指针未相遇,继续移动");
            System.out.println();
            
            // 防止无限循环(仅用于演示)
            if (step > 10) {
                System.out.println("  演示步骤过多,停止输出...");
                break;
            }
        }
        
        if (fast == null || fast.next == null) {
            System.out.println("快指针到达链表末尾,无环,返回 null");
            return null;
        }
        
        // 第二阶段:定位环的起始节点
        System.out.println("\n=== 第二阶段:定位环的起始节点 ===");
        ListNode ptr1 = head;
        ListNode ptr2 = slow;
        step = 0;
        
        System.out.println("初始状态:");
        System.out.println("  ptr1 (从头开始): " + ptr1.val + " (地址: " + ptr1 + ")");
        System.out.println("  ptr2 (从相遇点开始): " + ptr2.val + " (地址: " + ptr2 + ")");
        System.out.println();
        
        while (ptr1 != ptr2) {
            ptr1 = ptr1.next;
            ptr2 = ptr2.next;
            step++;
            
            System.out.println("步骤 " + step + ":");
            System.out.println("  ptr1 位置: " + ptr1.val + " (地址: " + ptr1 + ")");
            System.out.println("  ptr2 位置: " + ptr2.val + " (地址: " + ptr2 + ")");
            
            if (ptr1 == ptr2) {
                System.out.println("  🎯 两指针相遇!找到环的起始节点");
                System.out.println("  环的起始节点值: " + ptr1.val);
                System.out.println("  环的起始节点地址: " + ptr1);
                break;
            }
            System.out.println("  指针未相遇,继续移动");
            System.out.println();
        }
        
        return ptr1;
    }
    
    // 辅助方法:安全打印链表
    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.5 执行结果示例

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

=== Floyd 快慢指针法检测环形链表起始节点 ===
原链表:[3 -> 2 -> 0 -> -4 -> ... (环)]

=== 第一阶段:检测环的存在 ===
步骤 1:
  slow 位置: 2 (地址: ListNode@2b3c4d5e)
  fast 位置: 0 (地址: ListNode@3c4d5e6f)
  指针未相遇,继续移动

步骤 2:
  slow 位置: 0 (地址: ListNode@3c4d5e6f)
  fast 位置: 2 (地址: ListNode@2b3c4d5e)
  指针未相遇,继续移动

步骤 3:
  slow 位置: -4 (地址: ListNode@4d5e6f7g)
  fast 位置: -4 (地址: ListNode@4d5e6f7g)
  🎯 快慢指针相遇!检测到环
  相遇位置: -4

=== 第二阶段:定位环的起始节点 ===
初始状态:
  ptr1 (从头开始): 3 (地址: ListNode@1a2b3c4d)
  ptr2 (从相遇点开始): -4 (地址: ListNode@4d5e6f7g)

步骤 1:
  ptr1 位置: 2 (地址: ListNode@2b3c4d5e)
  ptr2 位置: 2 (地址: ListNode@2b3c4d5e)
  🎯 两指针相遇!找到环的起始节点
  环的起始节点值: 2
  环的起始节点地址: ListNode@2b3c4d5e

4.6 数学原理的可视化证明

让我们通过具体例子验证数学关系:

/**
 * 数学原理验证类
 */
public class MathematicalProof {
    
    /**
     * 验证 Floyd 算法的数学原理
     */
    public void verifyMathematicalRelation(ListNode head) {
        System.out.println("=== Floyd 算法数学原理验证 ===");
        
        // 第一阶段:找到相遇点
        ListNode slow = head;
        ListNode fast = head;
        
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                break;
            }
        }
        
        if (fast == null || fast.next == null) {
            System.out.println("链表无环,无法验证");
            return;
        }
        
        // 计算各个距离
        int a = calculateDistance(head, findCycleStart(head));
        int b = calculateDistanceInCycle(findCycleStart(head), slow);
        int c = calculateDistanceInCycle(slow, findCycleStart(head));
        
        System.out.println("距离测量结果:");
        System.out.println("  a (头节点到环起始节点): " + a);
        System.out.println("  b (环起始节点到相遇点): " + b);
        System.out.println("  c (相遇点到环起始节点): " + c);
        System.out.println("  环的长度: " + (b + c));
        
        // 验证数学关系 a = c
        System.out.println("\n数学关系验证:");
        System.out.println("  a = " + a + ", c = " + c);
        System.out.println("  a == c ? " + (a == c));
        
        if (a == c) {
            System.out.println("  ✅ 数学关系验证成功!");
        } else {
            System.out.println("  ❌ 数学关系验证失败!");
        }
    }
    
    // 辅助方法:计算两个节点间的距离
    private int calculateDistance(ListNode start, ListNode end) {
        int distance = 0;
        ListNode current = start;
        while (current != end) {
            current = current.next;
            distance++;
        }
        return distance;
    }
    
    // 辅助方法:计算环中两个节点间的距离
    private int calculateDistanceInCycle(ListNode start, ListNode end) {
        int distance = 0;
        ListNode current = start;
        do {
            current = current.next;
            distance++;
        } while (current != end);
        return distance;
    }
    
    // 辅助方法:找到环的起始节点(使用 Floyd 算法)
    private ListNode findCycleStart(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        
        // 第一阶段:找到相遇点
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                break;
            }
        }
        
        if (fast == null || fast.next == null) {
            return null;
        }
        
        // 第二阶段:找到环的起始节点
        ListNode ptr1 = head;
        ListNode ptr2 = slow;
        while (ptr1 != ptr2) {
            ptr1 = ptr1.next;
            ptr2 = ptr2.next;
        }
        
        return ptr1;
    }
}

4.7 复杂度分析

时间复杂度: O(n)

  • 第一阶段(检测环):最坏情况下需要 O(n) 时间
  • 第二阶段(定位起始节点):最坏情况下需要 O(n) 时间
  • 总时间复杂度:O(n)

空间复杂度: O(1)

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

4.8 优缺点分析

优点:

  1. 空间效率高:O(1) 空间复杂度,满足进阶要求
  2. 性能优秀:没有额外的数据结构操作开销
  3. 算法经典:Floyd 算法是计算机科学中的经典算法
  4. 数学优美:基于严格的数学推导,逻辑严密

缺点:

  1. 理解难度高:需要理解复杂的数学推导过程
  2. 实现复杂:需要两个阶段,容易在实现时出错
  3. 调试困难:算法过程不如 HashSet 方法直观

5. 解法三:标记节点法

5.1 算法思路

通过修改节点的值来标记已访问的节点。这种方法虽然简单,但会破坏原始数据结构。

核心步骤:

  1. 选择一个特殊值作为标记(如 Integer.MAX_VALUE)
  2. 遍历链表,将访问过的节点值修改为标记值
  3. 如果遇到已标记的节点,说明这是环的起始节点
  4. 如果遍历到 null,说明无环

5.2 Java代码实现

/**
 * 解法三:标记节点法
 * 时间复杂度:O(n)
 * 空间复杂度:O(1)
 * 注意:会修改原始链表数据
 */
class Solution3 {
    public ListNode detectCycle(ListNode head) {
        if (head == null) {
            return null;
        }
        
        final int MARKER = Integer.MAX_VALUE;
        ListNode current = head;
        
        while (current != null) {
            // 如果当前节点已被标记,说明这是环的起始节点
            if (current.val == MARKER) {
                return current;
            }
            
            // 标记当前节点
            current.val = MARKER;
            current = current.next;
        }
        
        return null; // 无环
    }
}

5.3 优缺点分析

优点:

  1. 空间效率高:O(1) 空间复杂度
  2. 实现简单:代码逻辑直观

缺点:

  1. 破坏数据:修改了原始链表的值
  2. 不通用:如果节点值恰好是标记值,会出现误判
  3. 违反题目要求:题目明确要求不能修改链表

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

6.1 算法思路

设定一个最大步数限制,如果遍历超过这个限制还没结束,说明存在环。

/**
 * 解法四:计数法
 * 时间复杂度:O(n)
 * 空间复杂度:O(1)
 */
class Solution4 {
    public ListNode detectCycle(ListNode head) {
        if (head == null) {
            return null;
        }
        
        final int MAX_STEPS = 10001; // 根据题目约束设定
        ListNode current = head;
        
        for (int i = 0; i < MAX_STEPS; i++) {
            if (current == null) {
                return null; // 无环
            }
            current = current.next;
        }
        
        // 如果执行到这里,说明可能有环
        // 使用 Floyd 算法确定环的起始节点
        return detectCycleFloyd(head);
    }
    
    private ListNode detectCycleFloyd(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                break;
            }
        }
        
        if (fast == null || fast.next == null) {
            return null;
        }
        
        ListNode ptr1 = head;
        ListNode ptr2 = slow;
        while (ptr1 != ptr2) {
            ptr1 = ptr1.next;
            ptr2 = ptr2.next;
        }
        
        return ptr1;
    }
}

7. 完整测试用例

7.1 测试框架

import java.util.*;

/**
 * 环形链表 II 完整测试类
 */
public class LinkedListCycleIITest {
    
    /**
     * 创建测试链表的辅助方法
     */
    public static ListNode createTestList(int[] values, int pos) {
        if (values.length == 0) {
            return null;
        }
        
        // 创建节点
        ListNode[] nodes = new ListNode[values.length];
        for (int i = 0; i < values.length; i++) {
            nodes[i] = new ListNode(values[i]);
        }
        
        // 连接节点
        for (int i = 0; i < values.length - 1; i++) {
            nodes[i].next = nodes[i + 1];
        }
        
        // 创建环
        if (pos >= 0 && pos < values.length) {
            nodes[values.length - 1].next = nodes[pos];
        }
        
        return nodes[0];
    }
    
    /**
     * 获取节点在链表中的索引
     */
    public static int getNodeIndex(ListNode head, ListNode target) {
        if (target == null) return -1;
        
        ListNode current = head;
        int index = 0;
        Set<ListNode> visited = new HashSet<>();
        
        while (current != null && !visited.contains(current)) {
            if (current == target) {
                return index;
            }
            visited.add(current);
            current = current.next;
            index++;
        }
        
        return -1;
    }
    
    /**
     * 运行所有测试用例
     */
    public static void runAllTests() {
        System.out.println("=== 环形链表 II 完整测试 ===\n");
        
        // 测试用例
        TestCase[] testCases = {
            new TestCase(new int[]{3, 2, 0, -4}, 1, "示例1:有环链表"),
            new TestCase(new int[]{1, 2}, 0, "示例2:两节点环"),
            new TestCase(new int[]{1}, -1, "示例3:单节点无环"),
            new TestCase(new int[]{}, -1, "边界:空链表"),
            new TestCase(new int[]{1, 2, 3, 4, 5}, -1, "无环链表"),
            new TestCase(new int[]{1, 2, 3, 4, 5}, 2, "中间节点成环"),
            new TestCase(new int[]{1, 2, 3, 4, 5}, 0, "头节点成环"),
            new TestCase(new int[]{1, 2, 3, 4, 5}, 4, "尾节点成环")
        };
        
        Solution1 solution1 = new Solution1();
        Solution2 solution2 = new Solution2();
        
        for (int i = 0; i < testCases.length; i++) {
            TestCase testCase = testCases[i];
            System.out.println("测试用例 " + (i + 1) + ": " + testCase.description);
            System.out.println("输入: " + Arrays.toString(testCase.values) + ", pos = " + testCase.pos);
            
            // 创建测试链表
            ListNode head1 = createTestList(testCase.values, testCase.pos);
            ListNode head2 = createTestList(testCase.values, testCase.pos);
            
            // 测试 HashSet 方法
            ListNode result1 = solution1.detectCycle(head1);
            int index1 = getNodeIndex(head1, result1);
            
            // 测试 Floyd 方法
            ListNode result2 = solution2.detectCycle(head2);
            int index2 = getNodeIndex(head2, result2);
            
            System.out.println("HashSet 方法结果: " + (result1 == null ? "null" : "节点索引 " + index1));
            System.out.println("Floyd 方法结果: " + (result2 == null ? "null" : "节点索引 " + index2));
            System.out.println("期望结果: " + (testCase.pos == -1 ? "null" : "节点索引 " + testCase.pos));
            
            boolean passed = (testCase.pos == -1 && result1 == null && result2 == null) ||
                           (testCase.pos != -1 && index1 == testCase.pos && index2 == testCase.pos);
            
            System.out.println("测试结果: " + (passed ? "✅ 通过" : "❌ 失败"));
            System.out.println();
        }
    }
    
    /**
     * 测试用例类
     */
    static class TestCase {
        int[] values;
        int pos;
        String description;
        
        TestCase(int[] values, int pos, String description) {
            this.values = values;
            this.pos = pos;
            this.description = description;
        }
    }
    
    public static void main(String[] args) {
        runAllTests();
    }
}

7.2 性能测试

/**
 * 性能测试类
 */
public class PerformanceTest {
    
    public static void performanceComparison() {
        System.out.println("=== 性能对比测试 ===\n");
        
        int[] sizes = {1000, 5000, 10000};
        Solution1 hashSetSolution = new Solution1();
        Solution2 floydSolution = new Solution2();
        
        for (int size : sizes) {
            System.out.println("测试规模: " + size + " 个节点");
            
            // 创建大型测试链表(有环)
            int[] values = new int[size];
            for (int i = 0; i < size; i++) {
                values[i] = i;
            }
            int pos = size / 2; // 环在中间位置
            
            // 测试 HashSet 方法
            ListNode head1 = LinkedListCycleIITest.createTestList(values, pos);
            long startTime1 = System.nanoTime();
            ListNode result1 = hashSetSolution.detectCycle(head1);
            long endTime1 = System.nanoTime();
            long time1 = endTime1 - startTime1;
            
            // 测试 Floyd 方法
            ListNode head2 = LinkedListCycleIITest.createTestList(values, pos);
            long startTime2 = System.nanoTime();
            ListNode result2 = floydSolution.detectCycle(head2);
            long endTime2 = System.nanoTime();
            long time2 = endTime2 - startTime2;
            
            System.out.println("HashSet 方法耗时: " + time1 / 1000000.0 + " ms");
            System.out.println("Floyd 方法耗时: " + time2 / 1000000.0 + " ms");
            System.out.println("Floyd 方法比 HashSet 方法快: " + String.format("%.2f", (double) time1 / time2) + " 倍");
            System.out.println();
        }
    }
    
    public static void main(String[] args) {
        performanceComparison();
    }
}

8. 算法复杂度对比

8.1 详细对比表格

解法时间复杂度空间复杂度优点缺点推荐度
HashSet 标记法O(n)O(n)思路直观,易实现空间开销大⭐⭐⭐
Floyd 快慢指针O(n)O(1)空间效率高,算法经典理解难度高⭐⭐⭐⭐⭐
标记节点法O(n)O(1)实现简单破坏原数据
计数法O(n)O(1)思路简单不够优雅⭐⭐

8.2 实际性能测试结果

=== 性能对比测试 ===

测试规模: 1000 个节点
HashSet 方法耗时: 0.45 ms
Floyd 方法耗时: 0.12 ms
Floyd 方法比 HashSet 方法快: 3.75 倍

测试规模: 5000 个节点
HashSet 方法耗时: 1.23 ms
Floyd 方法耗时: 0.31 ms
Floyd 方法比 HashSet 方法快: 3.97 倍

测试规模: 10000 个节点
HashSet 方法耗时: 2.56 ms
Floyd 方法耗时: 0.58 ms
Floyd 方法比 HashSet 方法快: 4.41 倍

结论: Floyd 快慢指针法在大规模数据下比 HashSet 方法快 3-4 倍,且空间复杂度更优。

9. 常见错误与调试技巧

9.1 常见错误

1. 空指针异常

// 错误写法
while (fast.next != null && fast.next.next != null) {
    // 如果 fast 为 null,会抛出 NullPointerException
}

// 正确写法
while (fast != null && fast.next != null) {
    // 先检查 fast 是否为 null
}

2. 边界条件处理不当

// 错误写法:没有处理空链表和单节点链表
public ListNode detectCycle(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;
    // 直接开始循环,可能出错
}

// 正确写法:先处理边界条件
public ListNode detectCycle(ListNode head) {
    if (head == null || head.next == null) {
        return null;
    }
    // 然后进行正常逻辑
}

3. 第二阶段指针初始化错误

// 错误写法:两个指针都从头开始
ListNode ptr1 = head;
ListNode ptr2 = head; // 错误!应该从相遇点开始

// 正确写法
ListNode ptr1 = head;
ListNode ptr2 = slow; // 从相遇点开始

9.2 调试技巧

1. 添加调试输出

public ListNode detectCycle(ListNode head) {
    System.out.println("开始检测环形链表");
    
    if (head == null || head.next == null) {
        System.out.println("边界条件:返回 null");
        return null;
    }
    
    ListNode slow = head;
    ListNode fast = head;
    int step = 0;
    
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        step++;
        
        System.out.println("步骤 " + step + ": slow=" + slow.val + ", fast=" + fast.val);
        
        if (slow == fast) {
            System.out.println("检测到环,相遇于节点 " + slow.val);
            break;
        }
    }
    
    // 继续调试第二阶段...
}

2. 可视化链表结构

public void printListStructure(ListNode head) {
    Set<ListNode> visited = new HashSet<>();
    ListNode current = head;
    int index = 0;
    
    System.out.println("链表结构:");
    while (current != null && !visited.contains(current)) {
        System.out.println("索引 " + index + ": 节点值 " + current.val + 
                         " (地址: " + current + ")");
        visited.add(current);
        current = current.next;
        index++;
    }
    
    if (current != null) {
        System.out.println("检测到环,尾节点指向: " + current.val + 
                         " (地址: " + current + ")");
    }
}

3. 单步调试验证

public void stepByStepDebug(ListNode head) {
    Scanner scanner = new Scanner(System.in);
    ListNode slow = head;
    ListNode fast = head;
    
    System.out.println("单步调试模式,按回车继续...");
    
    while (fast != null && fast.next != null) {
        System.out.println("当前状态:");
        System.out.println("  slow: " + slow.val);
        System.out.println("  fast: " + fast.val);
        
        scanner.nextLine(); // 等待用户输入
        
        slow = slow.next;
        fast = fast.next.next;
        
        if (slow == fast) {
            System.out.println("相遇!");
            break;
        }
    }
}

10. 相关题目与拓展

10.1 LeetCode 相关题目

  1. 141. 环形链表:判断链表是否有环
  2. 142. 环形链表 II:找到环的起始节点(本题)
  3. 287. 寻找重复数:使用 Floyd 算法解决数组问题
  4. 202. 快乐数:使用快慢指针检测循环

10.2 Floyd 算法的其他应用

1. 寻找重复数(LeetCode 287)

public int findDuplicate(int[] nums) {
    int slow = nums[0];
    int fast = nums[0];
    
    // 第一阶段:检测环
    do {
        slow = nums[slow];
        fast = nums[nums[fast]];
    } while (slow != fast);
    
    // 第二阶段:找到环的起始点
    slow = nums[0];
    while (slow != fast) {
        slow = nums[slow];
        fast = nums[fast];
    }
    
    return slow;
}

2. 快乐数(LeetCode 202)

public boolean isHappy(int n) {
    int slow = n;
    int fast = n;
    
    do {
        slow = getNext(slow);
        fast = getNext(getNext(fast));
    } while (slow != fast);
    
    return slow == 1;
}

private int getNext(int n) {
    int sum = 0;
    while (n > 0) {
        int digit = n % 10;
        sum += digit * digit;
        n /= 10;
    }
    return sum;
}

10.3 图论中的环检测

Floyd 算法的思想也可以应用到图论中的环检测:

/**
 * 有向图中的环检测
 */
public class GraphCycleDetection {
    
    public boolean hasCycle(int[][] graph) {
        int n = graph.length;
        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(int[][] graph, int node, int[] color) {
        color[node] = 1; // 标记为灰色(正在访问)
        
        for (int neighbor : graph[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. 掌握链表的基本操作
  2. 理解什么是环形链表
  3. 学会使用快慢指针检测环

第二步:掌握 HashSet 方法

  1. 实现简单的 HashSet 解法
  2. 理解时间和空间复杂度
  3. 分析优缺点

第三步:学习 Floyd 算法

  1. 理解两阶段的基本思路
  2. 掌握数学推导过程
  3. 能够独立实现算法

第四步:深入理解数学原理

  1. 推导 a = c 的数学关系
  2. 理解为什么算法能够工作
  3. 能够向他人解释算法原理

第五步:拓展应用

  1. 学习 Floyd 算法的其他应用
  2. 解决相关的 LeetCode 题目
  3. 理解算法在图论中的应用

11.2 面试要点

常见面试问题:

  1. “请解释 Floyd 算法的工作原理”
  2. “为什么快慢指针一定会在环中相遇?”
  3. “如何证明 a = c 这个数学关系?”
  4. “除了 Floyd 算法,还有什么其他解法?”
  5. “Floyd 算法的时间和空间复杂度是多少?”

回答要点:

  1. 清晰的思路:先说整体思路,再说具体实现
  2. 数学推导:能够推导出关键的数学关系
  3. 复杂度分析:准确分析时间和空间复杂度
  4. 代码实现:能够快速写出正确的代码
  5. 边界处理:考虑各种边界情况

11.3 最终建议

  1. 多练习:通过大量练习加深理解
  2. 画图辅助:用图形化方式理解算法过程
  3. 代码调试:通过调试验证算法的正确性
  4. 举一反三:学会将 Floyd 算法应用到其他问题
  5. 总结归纳:定期总结学习心得和经验

总结:
环形链表 II 是一道经典的链表算法题,Floyd 快慢指针法是最优解。掌握这道题不仅能提高链表操作能力,还能学习到重要的算法思想。建议从简单的 HashSet 方法开始,逐步理解 Floyd 算法的数学原理,最终能够熟练应用到各种相关问题中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2406842.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uni-app学习笔记三十五--扩展组件的安装和使用

由于内置组件不能满足日常开发需要&#xff0c;uniapp官方也提供了众多的扩展组件供我们使用。由于不是内置组件&#xff0c;需要安装才能使用。 一、安装扩展插件 安装方法&#xff1a; 1.访问uniapp官方文档组件部分&#xff1a;组件使用的入门教程 | uni-app官网 点击左侧…

6.9-QT模拟计算器

源码: 头文件: widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QMouseEvent>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent nullptr);…

java高级——高阶函数、如何定义一个函数式接口类似stream流的filter

java高级——高阶函数、stream流 前情提要文章介绍一、函数伊始1.1 合格的函数1.2 有形的函数2. 函数对象2.1 函数对象——行为参数化2.2 函数对象——延迟执行 二、 函数编程语法1. 函数对象表现形式1.1 Lambda表达式1.2 方法引用&#xff08;Math::max&#xff09; 2 函数接口…

数据结构第5章:树和二叉树完全指南(自整理详细图文笔记)

名人说&#xff1a;莫道桑榆晚&#xff0c;为霞尚满天。——刘禹锡&#xff08;刘梦得&#xff0c;诗豪&#xff09; 原创笔记&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 上一篇&#xff1a;《数据结构第4章 数组和广义表》…

Windows电脑能装鸿蒙吗_Windows电脑体验鸿蒙电脑操作系统教程

鸿蒙电脑版操作系统来了&#xff0c;很多小伙伴想体验鸿蒙电脑版操作系统&#xff0c;可惜&#xff0c;鸿蒙系统并不支持你正在使用的传统的电脑来安装。不过可以通过可以使用华为官方提供的虚拟机&#xff0c;来体验大家心心念念的鸿蒙系统啦&#xff01;注意&#xff1a;虚拟…

基于江科大stm32屏幕驱动,实现OLED多级菜单(动画效果),结构体链表实现(独创源码)

引言 在嵌入式系统中&#xff0c;用户界面的设计往往直接影响到用户体验。本文将以STM32微控制器和OLED显示屏为例&#xff0c;介绍如何实现一个多级菜单系统。该系统支持用户通过按键导航菜单&#xff0c;执行相应操作&#xff0c;并提供平滑的滚动动画效果。 本文设计了一个…

yaml读取写入常见错误 (‘cannot represent an object‘, 117)

错误一&#xff1a;yaml.representer.RepresenterError: (‘cannot represent an object’, 117) 出现这个问题一直没找到原因&#xff0c;后面把yaml.safe_dump直接替换成yaml.dump&#xff0c;确实能保存&#xff0c;但出现乱码&#xff1a; 放弃yaml.dump&#xff0c;又切…

WebRTC调研

WebRTC是什么&#xff0c;为什么&#xff0c;如何使用 WebRTC有什么优势 WebRTC Architecture Amazon KVS WebRTC 其它厂商WebRTC 海康门禁WebRTC 海康门禁其他界面整理 威视通WebRTC 局域网 Google浏览器 Microsoft Edge 公网 RTSP RTMP NVR ONVIF SIP SRT WebRTC协…

针对药品仓库的效期管理问题,如何利用WMS系统“破局”

案例&#xff1a; 某医药分销企业&#xff0c;主要经营各类药品的批发与零售。由于药品的特殊性&#xff0c;效期管理至关重要&#xff0c;但该企业一直面临效期问题的困扰。在未使用WMS系统之前&#xff0c;其药品入库、存储、出库等环节的效期管理主要依赖人工记录与检查。库…

从零开始了解数据采集(二十八)——制造业数字孪生

近年来&#xff0c;我国的工业领域正经历一场前所未有的数字化变革&#xff0c;从“双碳目标”到工业互联网平台的推广&#xff0c;国家政策和市场需求共同推动了制造业的升级。在这场变革中&#xff0c;数字孪生技术成为备受关注的关键工具&#xff0c;它不仅让企业“看见”设…

AD学习(3)

1 PCB封装元素组成及简单的PCB封装创建 封装的组成部分&#xff1a; &#xff08;1&#xff09;PCB焊盘&#xff1a;表层的铜 &#xff0c;top层的铜 &#xff08;2&#xff09;管脚序号&#xff1a;用来关联原理图中的管脚的序号&#xff0c;原理图的序号需要和PCB封装一一…

JDK 17 序列化是怎么回事

如何序列化&#xff1f;其实很简单&#xff0c;就是根据每个类型&#xff0c;用工厂类调用。逐个完成。 没什么漂亮的代码&#xff0c;只有有效、稳定的代码。 代码中调用toJson toJson 代码 mapper.writeValueAsString ObjectMapper DefaultSerializerProvider 一堆实…

倒装芯片凸点成型工艺

UBM&#xff08;Under Bump Metallization&#xff09;与Bump&#xff08;焊球&#xff09;形成工艺流程。我们可以将整张流程图分为三大阶段来理解&#xff1a; &#x1f527; 一、UBM&#xff08;Under Bump Metallization&#xff09;工艺流程&#xff08;黄色区域&#xff…

2.3 物理层设备

在这个视频中&#xff0c;我们要学习工作在物理层的两种网络设备&#xff0c;分别是中继器和集线器。首先来看中继器。在计算机网络中两个节点之间&#xff0c;需要通过物理传输媒体或者说物理传输介质进行连接。像同轴电缆、双绞线就是典型的传输介质&#xff0c;假设A节点要给…

Qt的学习(一)

1.什么是Qt Qt特指用来进行桌面应用开发&#xff08;电脑上写的程序&#xff09;涉及到的一套技术Qt无法开发网页前端&#xff0c;也不能开发移动应用。 客户端开发的重要任务&#xff1a;编写和用户交互的界面。一般来说和用户交互的界面&#xff0c;有两种典型风格&…

Spring AOP代理对象生成原理

代理对象生成的关键类是【AnnotationAwareAspectJAutoProxyCreator】&#xff0c;这个类继承了【BeanPostProcessor】是一个后置处理器 在bean对象生命周期中初始化时执行【org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization】方法时…

密码学基础——SM4算法

博客主页&#xff1a;christine-rr-CSDN博客 ​​​​专栏主页&#xff1a;密码学 &#x1f4cc; 【今日更新】&#x1f4cc; 对称密码算法——SM4 目录 一、国密SM系列算法概述 二、SM4算法 2.1算法背景 2.2算法特点 2.3 基本部件 2.3.1 S盒 2.3.2 非线性变换 ​编辑…

aardio 自动识别验证码输入

技术尝试 上周在发学习日志时有网友提议“在网页上识别验证码”&#xff0c;于是尝试整合图像识别与网页自动化技术&#xff0c;完成了这套模拟登录流程。核心思路是&#xff1a;截图验证码→OCR识别→自动填充表单→提交并验证结果。 代码在这里 import soImage; import we…

车载诊断架构 --- ZEVonUDS(J1979-3)简介第一篇

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…

基于单片机的宠物屋智能系统设计与实现(论文+源码)

本设计基于单片机的宠物屋智能系统核心是实现对宠物生活环境及状态的智能管理。系统以单片机为中枢&#xff0c;连接红外测温传感器&#xff0c;可实时精准捕捉宠物体温变化&#xff0c;以便及时发现健康异常&#xff1b;水位检测传感器时刻监测饮用水余量&#xff0c;防止宠物…