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

前言

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

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

相关推荐
David猪大卫8 小时前
【C++修炼】map 和 set 的使用
c语言·开发语言·数据结构·c++·经验分享·笔记·学习
vvilkim10 小时前
数据结构类型、区别与应用场景:一篇讲透选型逻辑
数据结构
阿卡好可爱14 小时前
Lua 热更新不生效:改完文件线上仍是旧逻辑,package.loaded 与 upvalue 重绑定详解
数据结构·分布式·后端·架构·lua
心仪久了会沦陷1 天前
C语言数据结构入门——树与二叉树详解:概念、堆实现与链式遍历
c语言·数据结构·算法
码少女1 天前
数据结构——图的基础
数据结构
Severus_black1 天前
【C++初阶】list使用及相关实现,思路顺滑流畅!
c语言·数据结构·c++·stl·list
二本咕咕-机械转码2 天前
FreeRTOS深入解析(八)IPC机制之FromISR详解
数据结构·单片机·链表
1-1=02 天前
C# 数据结构和特殊类型
数据结构·c#·.net·.netcore
希和20272 天前
C语言程序设计题|有序数组插入数字冒泡排序
c语言·数据结构·算法
布莱克6052 天前
C++ 中 map 的 insert 与 emplace:key 已存在时会覆盖吗?
开发语言·数据结构·c++·算法·map