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

前言

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

  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. 场景选择:刷题快速写答案用哈希;正式面试、追求最优空间复杂度必须掌握快慢指针弗洛伊德算法。

相关推荐
_Narcissus_1 天前
排序算法总结表格
c语言·数据结构·c++·笔记·算法·排序算法·总结
一只小小的芙厨1 天前
前缀和与差分
数据结构·算法
一只小小的芙厨1 天前
最短路总结
数据结构·算法
来一碗刘肉面1 天前
有向无环图 DAG(描述表达式)
数据结构
郝学胜_神的一滴1 天前
干货版《算法导论》18:遍历增删原理、时间复杂度与集合序列实现全解
数据结构·算法
BK贝壳1 天前
Python底层:1亿个布尔值缓存失效标记,list爆内存numpy爆拷贝,bool-hybrid-array混合存储实测
数据结构·python·缓存·性能优化·list·numpy
_Narcissus_1 天前
B树概念及操作笔记(含完整代码实现)
c语言·数据结构·数据库·c++·笔记·b树·算法
疯狂打码的少年1 天前
【数据结构】图的应用:最短路径(Dijkstra / Floyd)
数据结构·笔记·算法
Fa_Mian_Tuan1 天前
图论基础 | 邻接多重表原理剖析 + 完整可运行C语言代码(插边/删边/释放)
c语言·数据结构·笔记·算法·图论
学习星球1 天前
【LeetCode算法题精讲】二分查找精讲
java·数据结构·算法·leetcode·职场和发展·图搜索