数据结构与算法篇-链表环检测

链表环检测

问题描述

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

若链表中存在某个节点,通过不断跟随 next 引用的方式能再次到达该节点,则说明链表存在环。环可以出现在链表的任意位置,并非一定形成于链表尾部。

若检测到环,返回 true;否则返回 false

挑战要求:求解过程中不得使用任何额外的数据结构(如数组、哈希表等)。

示例

示例 1:存在环

txt 复制代码
Input: head = [3,2,0,-4], cycle connects tail to node index 1
Output: true

Visual:
3 -> 2 -> 0 -> -4
     ↑         |
     +---------+

Explanation: There is a cycle where the tail connects back to the 2nd node (0-indexed).

示例 2:存在环(简单)

txt 复制代码
Input: head = [1,2], cycle connects tail to node index 0
Output: true

Visual:
1 -> 2
↑    |
+----+

Explanation: There is a cycle where the tail connects back to the first node.

示例 3:无环

txt 复制代码
Input: head = [1,2,3,4,5]
Output: false

Visual:
1 -> 2 -> 3 -> 4 -> 5 -> null

Explanation: The list ends with null, so there is no cycle.

示例 4:单节点,无环

txt 复制代码
Input: head = [1]
Output: false

Visual:
1 -> null

Explanation: Single node pointing to null has no cycle.

示例 5:单节点自环

txt 复制代码
Input: head = [1], cycle connects to itself
Output: true

Visual:
1 --+
↑   |
+---+

Explanation: The single node points to itself, creating a cycle.

测试用例

LinkedListCycleTest.java

朴素版实现1

借助外部存储,记录节点已访问状态

java 复制代码
public static boolean hasCycleNavive1(ListNode head) {
    HashSet<ListNode> visited = new HashSet();

    ListNode current = head;
    while (current != null) {
        if (visited.contains(current)) {
            return true;
        }
        visited.add(current);
        current = current.next;
    }
    return false;
}

朴素版 2

修改节点数据,标记其已访问状态

java 复制代码
public static boolean hasCycleNavive2(ListNode head) {
    int visitedMark = Integer.MIN_VALUE;
    ListNode current = head;
    while (current != null) {
        if (current.val == visitedMark) {
            return true;
        }
        current.val = visitedMark;
        current = current.next;
    }
    return false;
}

换个思路

前面两个朴素版实现都是通过记忆来实现环检测,那么,除此外,还有其他方法吗?

类比:想象你在一个环形操场跑步。如果你和你的朋友同时出发,但你的速度比他快。

  • 如果操场是直道,你会先到达终点,两人永远不会再碰面。
  • 如果操场是环形,只要跑得足够久,你一定会 "套圈" 并在后面追上他。

在链表中,我们用两个指针模拟这种 "速度差":

  • 慢指针 (slow):每次走 1 步。
  • 快指针 (fast):每次走 2 步。

在 Naive 1 中,我们要靠"记忆"来发现重复;

在双指针中,我们靠**"追赶"**来发现重复。

  • 这种方法不需要存储任何历史记录,只需要关心当前的两个位置。

双指针版本

java 复制代码
public static boolean hasCycle(ListNode head) {
    // Handle edge cases
    if (head == null) {
        return false;
    }
    // Handle edge cases
    if (head.next == null) {
        return false;
    }

    // Initialize two pointers
    ListNode slow = head; // Moves 1 step at a time
    ListNode fast = head; // Moves 2 steps at a time

    while (fast != null && fast.next != null) {
        if (fast == slow) {
            return true;
        }
        slow = slow.next; // Move slow pointer 1 step
        fast = fast.next.next; // Move fast pointer 2 step
    }
    // Fast pointer reached end (null), no cycle
    return false;
}

参考资料