链表判环 & 寻找环入口 两种解法详解

前言

链表环问题是链表经典高频面试题,分为两个基础题型:

  1. hasCycle:判断单链表中是否存在环;
  2. detectCycle:若链表有环,返回环的起始节点,无环返回null

单链表单向遍历,无法直接回头判断重复节点,业界主流两种解法:哈希集合法 (直观易写)、快慢指针(弗洛伊德判圈算法)(空间最优,面试核心考点)。

统一链表节点定义:

复制代码
class ListNode {
    int val;
    ListNode next;
    ListNode(int x) {
        val = x;
        next = null;
    }
}

一、哈希集合解法(判环 + 找环起点通用)

核心思路

利用HashSet存储已经遍历过的节点对象(注意:存节点引用,不是节点值,不同节点允许值相同):

  1. 遍历链表指针依次向后移动;

  2. 每一步先判断当前节点是否在集合中:

    • 存在:链表有环,当前节点就是环入口;
    • 不存在:将当前节点存入集合,指针后移;
  3. 指针走到null,说明链表无环。

优缺点

  • 优点:逻辑极简,无需数学推导,判环、找环入口代码几乎一致,上手零门槛;
  • 缺点:需要额外哈希集合存储所有节点,空间复杂度 O(n)

1. 判断链表是否有环 hasCycle

复制代码
public class Solution {
    public boolean hasCycle(ListNode head) {
        Set<ListNode> set = new HashSet<>();
        ListNode p = head;
        while (p != null) {
            // 当前节点已访问过,存在环
            if (set.contains(p)) {
                return true;
            }
            set.add(p);
            p = p.next;
        }
        // 遍历到末尾无环
        return false;
    }
}

2. 找到环的起始节点 detectCycle

复制代码
public class Solution {
    public ListNode detectCycle(ListNode head) {
        Set<ListNode> set = new HashSet<>();
        ListNode p = head;
        while (p != null) {
            // 重复节点即为环入口,直接返回
            if (set.contains(p)) {
                return p;
            }
            set.add(p);
            p = p.next;
        }
        // 无环返回null
        return null;
    }
}

二、快慢指针(弗洛伊德判圈算法,空间 O (1) 最优解)

1. 快慢指针判断是否存在环 hasCycle

核心思路

快慢指针模拟跑道追及问题:

  • 慢指针slow:每次走 1 步;
  • 快指针fast:每次走 2 步;
  • 若无环:快指针会率先走到链表末尾null,循环终止返回false
  • 若存在环:快慢指针一定会在环内某处相遇,相遇即证明有环。

边界提前特判:链表为空 / 只有单个节点,直接无环。

优缺点

  • 优点:仅两个临时指针,空间复杂度 O(1),时间 O(n),面试最优解法;
  • 缺点:需要理解追及逻辑,无法直接得到环入口,找起点需要额外推导。

完整代码

复制代码
public class Solution {
    public boolean hasCycle(ListNode head) {
        // 空链表/单个节点一定无环
        if (head == null || head.next == null) {
            return false;
        }
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            // 快慢指针相遇,存在环
            if (fast == slow) {
                return true;
            }
        }
        // 快指针走到链表尾部,无环
        return false;
    }
}

补充:快慢指针寻找环入口(拓展考点)

数学推导核心结论

  1. 快慢指针第一次相遇后,将慢指针重置到链表头;

  2. 快慢指针同时改为每次走 1 步

  3. 二者再次相遇的节点,就是环的起始节点。

    推导本质:头部到环入口距离 = 相遇点到环入口的剩余环长。

detectCycle 快慢指针完整代码

复制代码
public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        // 第一步:快慢指针找相遇点
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            // 两指针相遇
            if (slow == fast) {
                // 慢指针放回头部
                slow = head;
                // 同步一步走,相遇即为环起点
                while (slow != fast) {
                    slow = slow.next;
                    fast = fast.next;
                }
                return slow;
            }
        }
        // 无环
        return null;
    }
}

三、两种解法综合对比

方案 时间复杂度 空间复杂度 优势 短板
哈希集合 O(n) O(n) 逻辑简单,可直接返回环入口 占用额外内存,长链表开销大
快慢指针 O(n) O(1) 常数空间,性能最优 需要数学推导,找入口多一轮遍历

四、面试核心考点总结

  1. 哈希表注意点:存入节点引用而非数值,多个节点值相同但地址不同不算环;

  2. 快慢指针边界 :循环条件必须是fast != null && fast.next != null,防止fast.next.next空指针报错;

  3. 判环 vs 找入口区分

    • 只判断有无环:快慢指针相遇直接返回true
    • 寻找环起点:相遇后慢指针归头,同速前进二次相遇才是入口;
  4. 场景选择:刷题快速写答案用哈希;正式面试、追求最优空间复杂度必须掌握快慢指针弗洛伊德算法。

相关推荐
罗超驿3 小时前
2.算法效率的核心密码:时间复杂度和空间复杂度详解
java·数据结构·算法
:-)4 小时前
算法-堆排序
数据结构·算法·排序算法
j7~7 小时前
【数据结构初阶】顺序表增删查改代码实现--详解
数据结构·顺序表·动态顺序表·静态顺序表
枕星而眠7 小时前
【数据结构】红黑树入门指南
运维·数据结构·c++·后端
:-)9 小时前
基础算法-选择排序
数据结构·算法·排序算法
粘稠的浆糊9 小时前
[AtCoder - abc465_d ]X to Y题解
数据结构·c++·算法
海清河晏1119 小时前
数据结构 | 二叉平衡搜索树
开发语言·数据结构·visual studio
兰令水10 小时前
hot100【acm版】【2026.7.11/12打卡-java版本】
java·开发语言·数据结构·算法·职场和发展
叩码以求索12 小时前
使用next数组加速匹配过程
java·数据结构·算法
小欣加油12 小时前
leetcode1331 数组序号转换
数据结构·c++·算法·leetcode·职场和发展