力扣解题-141. 环形链表

0 阅读6分钟

力扣解题-141. 环形链表

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

示例 1:

image.png

输入:head = [3,2,0,-4], pos = 1

输出:true

解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

image.png

输入:head = [1,2], pos = 0

输出:true

解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

image.png

输入:head = [1], pos = -1

输出:false

解释:链表中没有环。

提示:

链表中节点的数目范围是 [0, 10⁴]

-10⁵ <= Node.val <= 10⁵

pos 为 -1 或者链表中的一个 有效索引 。

进阶:你能用 O(1)(即,常量)内存解决此问题吗?

Related Topics

哈希表、链表、双指针


第一次解答

解题思路

核心方法:哈希集合记录访问过的节点,通过HashSet存储遍历过程中访问过的链表节点,若遍历到重复节点则说明存在环,逻辑直观但需要O(n)的额外空间。

核心逻辑拆解

判断链表是否有环的核心是“是否会重复访问同一个节点”:

  1. 初始化:创建空的HashSet用于存储已访问的节点,定义current指针从链表头节点head开始遍历;
  2. 遍历链表
    • current为null(遍历到链表尾部),说明无环,返回false;
    • 检查current是否在HashSet中:
      • 存在:说明之前访问过该节点,链表有环,返回true;
      • 不存在:将current加入HashSet,current移动到下一个节点(current.next);
  3. 终止条件:要么找到重复节点(有环),要么遍历到null(无环)。
性能说明
  • 时间复杂度:O(n)(每个节点最多被访问一次,HashSet的containsadd操作均为O(1));
  • 空间复杂度:O(n)(最坏情况链表无环,HashSet存储所有n个节点);
  • 优势:
    1. 逻辑简单易懂,新手易上手;
    2. 遍历过程中发现环立即返回,无需遍历完整链表;
  • 劣势:额外使用HashSet存储节点,空间复杂度未满足进阶的O(1)要求。
    public boolean hasCycle(ListNode head) {
    Set<ListNode> visited = new HashSet<>();
    ListNode current = head;

    while (current != null) {
        // 如果当前节点已经访问过,说明有环
        if (visited.contains(current)) {
            return true;
        }
        // 记录当前节点
        visited.add(current);
        // 移动到下一个节点
        current = current.next;
    }

    // 遍历到 null,说明无环
    return false;
}

示例解答

解题思路

解法1:快慢指针法(弗洛伊德循环检测,最优解)

核心方法:双指针(慢指针+快指针),慢指针每次走一步,快指针每次走两步,若链表有环则快慢指针必会相遇;若无环则快指针会先遍历到null,时间复杂度O(n)、空间复杂度O(1),满足进阶要求。

核心原理铺垫

该方法的核心类比“操场跑步”:

  • 无环链表:快指针(跑得快的人)会先到达终点(null);
  • 有环链表:快慢指针进入环后,快指针会绕环追上慢指针(相遇);
  • 数学证明:设环的长度为L,快慢指针进入环时的距离差为d,快指针每次比慢指针多走1步,最多L步后必然相遇。
核心逻辑拆解
  1. 边界处理:若头节点head为null,直接返回false(空链表无环);
  2. 初始化指针:慢指针slow指向head,快指针fast指向head.next(避免初始状态快慢指针重合);
  3. 循环检测
    • slow != fast,进入循环:
      • 检查快指针是否到达终点(fast == nullfast.next == null),若是则返回false;
      • 慢指针走一步(slow = slow.next);
      • 快指针走两步(fast = fast.next.next);
    • slow == fast,说明相遇,返回true;
  4. 终止条件:要么快慢指针相遇(有环),要么快指针到null(无环)。
具体步骤(以示例1 head=[3,2,0,-4],pos=1为例)
  1. slow=3,fast=2;
  2. 第一次循环:slow=2,fast=0→-4→2;
  3. slow=2,fast=2,相遇,返回true。
性能说明
  • 时间复杂度:O(n)(无环时快指针遍历n个节点,有环时最多遍历n+L个节点,L≤n);
  • 空间复杂度:O(1)(仅使用两个指针变量,无额外存储);
  • 核心优势:
    1. 满足进阶的O(1)空间要求,内存开销极小;
    2. 无需额外数据结构,执行效率更高;
    3. 相遇时立即返回,无需遍历完整链表。
public boolean hasCycle(ListNode head) {
        if(head==null){
            return false;
        }
        ListNode slow=head;
        ListNode fast=head.next;
        while (slow!=fast){
            if(fast==null||fast.next==null){
                return false;
            }
            slow=slow.next;
            fast=fast.next.next;
        }
        return true;
    }
解法2:快慢指针简化版(更通用的初始化方式)

核心方法:快慢指针的另一种初始化方式(均从head开始),逻辑更简洁,避免head.next为空导致的空指针异常。

代码实现
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 (slow == fast) {
            return true;
        }
    }
    // 快指针到终点,无环
    return false;
}
优势说明
  • 初始化更通用:快慢指针均从head开始,避免head.next为null时的异常;
  • 逻辑更直观:循环条件直接检查快指针是否可达,相遇判断放在循环内,新手易理解;
  • 性能与原快慢指针法一致,仅代码风格差异。
解法3:节点标记法(另类思路)

核心方法:遍历过程中修改节点值/指针标记访问状态,将访问过的节点val设为特殊值(如Integer.MAX_VALUE),若遍历到该特殊值则说明有环,空间复杂度O(1)但会修改原链表。

代码实现
public boolean hasCycle(ListNode head) {
    ListNode current = head;
    while (current != null) {
        // 检测到标记,说明有环
        if (current.val == Integer.MAX_VALUE) {
            return true;
        }
        // 标记已访问的节点
        current.val = Integer.MAX_VALUE;
        current = current.next;
    }
    return false;
}
适用场景说明
  • 空间复杂度:O(1),满足进阶要求;
  • 局限性:
    1. 修改了原链表的节点值,破坏数据完整性;
    2. 若节点值本身包含Integer.MAX_VALUE,会导致误判;
  • 适用场景:仅作思路拓展,实际工程中禁止修改输入数据,因此不推荐使用。

总结

  1. 哈希集合法(第一次解答):O(n)时间+O(n)空间,逻辑直观但空间开销大,适合理解核心思路;
  2. 快慢指针法(最优解):O(n)时间+O(1)空间,满足进阶要求,无额外存储且执行高效,工程首选;
  3. 节点标记法:O(n)时间+O(1)空间,但修改原链表,仅作思路拓展;
  4. 关键技巧:
    • 环形检测优先用快慢指针法,是链表/数组循环检测的经典贪心策略;
    • 快慢指针初始化需注意:避免初始重合(如示例解法中fast=head.next),或在循环内先移动再判断;
    • 边界处理:空链表、单节点链表需直接返回false,避免空指针异常。