单链表判环

判定一个单链表是否存在环

给定一个链表,如果它是有环链表,实现一个算法返回环路的开头节点。若环不存在,请返回 null
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。

下面介绍两种解决方法:快慢指针法(Floyd 判圈算法)和哈希表法。链表节点定义如下:

1public class ListNode {
2    int val;
3    ListNode next;
4    ListNode(int x) {
5        val = x;
6        next = null;
7    }
8}

方法一:快慢指针法

思路

  1. 检测环是否存在
    使用两个指针:慢指针 slow 每次走一步,快指针 fast 每次走两步。如果链表有环,则快慢指针必然在环内相遇;如果无环,快指针会遇到 null

  2. 寻找环的入口
    当快慢指针相遇时,让其中一个指针(例如 slow)回到链表头,然后两个指针都每次走一步。它们下一次相遇的节点就是环的入口点。

    证明(简要说明):
    设链表头到环入口的距离为 L,环的长度为 C,相遇点距离环入口的距离为 D。相遇时,快指针走的距离为 L + mC + D,慢指针走的距离为 L + nC + D,并且快指针走的路程是慢指针的两倍,所以有: $$ 2(L + nC + D) = L + mC + D $$ 化简得到: $$ L + D = (m - 2n) \times C $$

    也就是说,从头到环入口的距离 L 等于从相遇点到环入口沿环走的距离(补全环一圈的剩余距离),因此当两个指针分别从链表头和相遇点出发以相同速度前进时,会在环入口相遇。

实现代码

Java

 1public class Solution {
 2    public ListNode detectCycle(ListNode head) {
 3        if (head == null) {
 4            return null;
 5        }
 6        
 7        ListNode slow = head;
 8        ListNode fast = head;
 9        boolean hasCycle = false;
10        
11        // 第一阶段:判断是否存在环
12        while (fast != null && fast.next != null) {
13            slow = slow.next;           // 慢指针走一步
14            fast = fast.next.next;      // 快指针走两步
15            
16            if (slow == fast) {         // 相遇,说明有环
17                hasCycle = true;
18                break;
19            }
20        }
21        
22        if (!hasCycle) {
23            return null;  // 无环,直接返回 null
24        }
25        
26        // 第二阶段:寻找环的入口
27        slow = head;  // 将慢指针移回链表头
28        while (slow != fast) {
29            slow = slow.next;
30            fast = fast.next;  // 两指针均一次走一步
31        }
32        
33        // 当两指针相遇时,就是环的入口点
34        return slow;
35    }
36}

C++

 1class Solution {
 2public:
 3    ListNode* detectCycle(ListNode* head) {
 4        if (head == nullptr) {
 5            return nullptr;
 6        }
 7        
 8        ListNode* slow = head;
 9        ListNode* fast = head;
10        bool hasCycle = false;
11        
12        // 第一阶段:判断是否存在环
13        while (fast != nullptr && fast->next != nullptr) {
14            slow = slow->next;             // 慢指针走一步
15            fast = fast->next->next;       // 快指针走两步
16            if (slow == fast) {            // 相遇,说明存在环
17                hasCycle = true;
18                break;
19            }
20        }
21        
22        if (!hasCycle) {
23            return nullptr;  // 无环,返回 nullptr
24        }
25        
26        // 第二阶段:寻找环的入口
27        slow = head;  // 慢指针回到链表头
28        while (slow != fast) {
29            slow = slow->next;
30            fast = fast->next;  // 两个指针均一次走一步
31        }
32        
33        // 此时 slow 和 fast 相遇,指向环的入口节点
34        return slow;
35    }
36};

方法二:哈希表法

思路

利用一个哈希表(或哈希集合)来记录已经遍历过的节点。遍历链表时:

  • 如果当前节点已在哈希集合中,则说明该节点是环的入口(第一次重复出现的节点)。
  • 如果遍历过程中遇到 null,则说明链表无环。

这种方法虽然简单直观,但需要额外的空间,其空间复杂度为 O(n)。

实现代码

Java

 1import java.util.HashSet;
 2import java.util.Set;
 3
 4public class Solution {
 5    public ListNode detectCycleWithHashTable(ListNode head) {
 6        Set<ListNode> visited = new HashSet<>();
 7        ListNode current = head;
 8        
 9        while (current != null) {
10            // 如果当前节点已经存在于集合中,则找到环的入口
11            if (visited.contains(current)) {
12                return current;
13            }
14            visited.add(current);
15            current = current.next;
16        }
17        
18        // 如果遍历结束都没有重复节点,则链表无环
19        return null;
20    }
21}

C++

 1#include <unordered_set>
 2
 3class Solution {
 4public:
 5    ListNode* detectCycleWithHashTable(ListNode* head) {
 6        std::unordered_set<ListNode*> visited;
 7        ListNode* current = head;
 8        
 9        while (current != nullptr) {
10            // 如果当前节点已经访问过,则找到了环的入口
11            if (visited.find(current) != visited.end()) {
12                return current;
13            }
14            visited.insert(current);
15            current = current->next;
16        }
17        
18        // 遍历结束仍未发现重复节点,说明链表无环
19        return nullptr;
20    }
21};

总结

  • 快慢指针法

    • 时间复杂度:O(n)
    • 空间复杂度:O(1)
    • 优点:不需要额外空间,效率高
    • 缺点:理解其证明和算法思路需要一定的数学推导
  • 哈希表法

    • 时间复杂度:O(n)
    • 空间复杂度:O(n)
    • 优点:实现简单,直观易懂
    • 缺点:需要额外的空间