算法题 环形链表

141. 环形链表

问题描述

给定一个链表,判断链表中是否有环。如果链表中有某个节点可以通过连续跟踪 next 指针再次到达,则链表中存在环。使用整数 pos 表示链表尾连接到链表中的位置(索引从 0 开始),若 pos = -1 则无环。

示例

输入:head = [3,2,0,-4], pos = 1 → 输出:true(尾部连接到第二个节点)
输入:head = [1,2], pos = 0 → 输出:true(尾部连接到第一个节点)
输入:head = [1], pos = -1 → 输出:false(无环)

算法思路:快慢指针(Floyd 判圈算法)

  1. 初始化指针
    • slow 慢指针每次移动 1 步
    • fast 快指针每次移动 2 步
  2. 遍历链表
    • fast 不为 nullfast.next 不为 null 时循环
    • 移动指针:slow = slow.nextfast = fast.next.next
  3. 判断相遇
    • slow == fast 说明有环
    • fast 遇到 null 说明无环

数学证明

  • 设环外长度 ( a ),环内相遇点距环入口 ( b ),环剩余长度 ( c )(环总长 ( b+c ))
  • slow 走 ( a+b ) 步时,fast 走 ( 2(a+b) ) 步
  • 快慢指针满足:( 2(a+b) = a + b + k(b+c) )(( k ) 为快指针绕环圈数)
  • 化简得:( a = (k-1)(b+c) + c )
  • 该等式恒成立,故必会相遇

代码实现

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        // 边界处理:空链表或单节点无环
        if (head == null || head.next == null) {
            return false;
        }
        
        ListNode slow = head;  // 慢指针(每次1步)
        ListNode fast = head;   // 快指针(每次2步)
        
        // 遍历链表(fast需检查两步)
        while (fast != null && fast.next != null) {
            slow = slow.next;          // 慢指针移动1步
            fast = fast.next.next;     // 快指针移动2步
            
            // 相遇说明有环
            if (slow == fast) {
                return true;
            }
        }
        return false;  // 遍历结束无环
    }
}

算法分析

  • 时间复杂度:( O(n) )
    • 无环时:快指针先到达尾部,遍历 ( n/2 ) 次
    • 有环时:慢指针入环前走 ( a ) 步,入环后走 ( c ) 步被追上(总步数小于 ( n ))
  • 空间复杂度:( O(1) )
    • 仅使用两个指针

关键点

  1. 终止条件

    • fast == null(偶数节点无环)
    • fast.next == null(奇数节点无环)
  2. 指针起点

    • 同起点 head 开始(也可让 slow=head, fast=head.next
  3. 移动顺序

    • 先移动指针再判断相遇(若先判断则初始状态 slow==fast

算法过程

无环链表:1 → 2 → 3 → null
步骤:
  slow:1 → 2 → 3
  fast:1 → 3 → null(终止)

有环链表:3 → 2 → 0 → -4 → 2(环)
步骤:
  slow:3 → 2 → 0 → -4
  fast:3 → 0 → 2 → -4 → 0 → 2
  在 -4 处未相遇,在 2 处相遇

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1:有环(尾部连接位置1)
    ListNode node1 = new ListNode(3);
    ListNode node2 = new ListNode(2);
    ListNode node3 = new ListNode(0);
    ListNode node4 = new ListNode(-4);
    node1.next = node2; node2.next = node3; node3.next = node4; node4.next = node2;
    System.out.println("Test 1: " + solution.hasCycle(node1)); // true
    
    // 测试用例2:有环(尾部连接位置0)
    ListNode node5 = new ListNode(1);
    ListNode node6 = new ListNode(2);
    node5.next = node6; node6.next = node5;
    System.out.println("Test 2: " + solution.hasCycle(node5)); // true
    
    // 测试用例3:无环
    ListNode node7 = new ListNode(1);
    System.out.println("Test 3: " + solution.hasCycle(node7)); // false
    
    // 测试用例4:长链表无环
    ListNode head = new ListNode(0);
    ListNode cur = head;
    for (int i = 1; i <= 1000; i++) {
        cur.next = new ListNode(i);
        cur = cur.next;
    }
    System.out.println("Test 4: " + solution.hasCycle(head)); // false
}

常见问题

  1. 为什么快指针走两步?走三步可以吗?

    • 走两步必会相遇(速度差为1,能追上)。走三步可能错过(速度差为2,若环长奇数可能永远追不上)。
  2. 如何找到环的入口?

    • 当快慢指针相遇后,将快指针移回链表头,然后两指针每次各走一步,再次相遇点即为环入口(根据公式 ( a = c ) 推导)。
  3. 边界条件处理

    • 空链表直接返回 false
    • 单节点需检查 next 是否为 null(无环)
  4. 为什么先移动指针再判断?

    • 若先判断相遇,初始状态 slowfast 都指向 head 会立即返回 true(错误判环)。

142. 环形链表 II

问题描述

给定一个链表的头节点 head,返回链表开始入环的第一个节点。如果链表无环,则返回 null

示例

输入:head = [3,2,0,-4],pos = 1(尾部连接到索引1节点)
输出:返回索引为1的节点(值为2

算法思路:快慢指针 + 数学推导

  1. 判断是否有环
    • 快指针(每次2步)和慢指针(每次1步)从起点出发
    • 若相遇则说明有环
  2. 寻找环入口
    • 相遇后将快指针移回链表头
    • 两指针每次各走1步,再次相遇点即为环入口

数学证明

  • 设头节点到环入口距离为 ( a )
  • 环入口到相遇点距离为 ( b )
  • 相遇点到环入口距离为 ( c )(环长 ( L = b + c ))
  • 第一次相遇时:
    • 慢指针:( a + b )
    • 快指针:( a + b + kL )(( k ) 为绕环圈数)
  • 速度关系:( 2(a + b) = a + b + kL )
    → ( a + b = kL )
    → ( a = kL - b = (k-1)L + c )
  • 表明:从链表头走 ( a ) 步 = 从相遇点走 ( c ) 步 + ( (k-1) ) 圈

代码实现

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        // 边界处理:空链表或单节点无环
        if (head == null || head.next == null) {
            return null;
        }
        
        ListNode slow = head;  // 慢指针(每次1步)
        ListNode fast = head;  // 快指针(每次2步)
        boolean hasCycle = false;
        
        // 第一步:判断是否有环
        while (fast != null && fast.next != null) {
            slow = slow.next;      // 慢指针移动1步
            fast = fast.next.next; // 快指针移动2步
            
            if (slow == fast) {
                hasCycle = true;
                break;
            }
        }
        
        // 无环直接返回
        if (!hasCycle) {
            return null;
        }
        
        // 第二步:寻找环入口
        fast = head;  // 快指针移回链表头
        while (slow != fast) {
            slow = slow.next;  // 两指针同步移动
            fast = fast.next;
        }
        return slow;  // 相遇点即为环入口
    }
}

算法分析

  • 时间复杂度:O(n)
    • 第一轮找相遇点最多走 n 步
    • 第二轮找入口最多走 n 步
  • 空间复杂度:O(1)
    • 仅使用两个指针

关键点

  1. 双指针初始位置

    • 同起点 head 开始
    • 若从 slow=head, fast=head.next 开始需调整相遇判断
  2. 移动规则

    • 第一轮:快2步/慢1步
    • 第二轮:双指针均1步
  3. 数学关系

    • ( a = (k-1)L + c ) 是关键推导
    • 环入口位置与圈数 ( k ) 无关

算法过程

        a        b
头节点 O → O → O → O ← 环入口
             ↑   ↓   c
             O ← O  相遇点

步骤:
1. 快慢指针在相遇点相遇
2. 快指针移回头节点
3. 两指针各走 a 步:
   - 快指针从头走 a 步到环入口
   - 慢指针走 a = (k-1)L + c 步也到环入口

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 构造带环链表 [3,2,0,-4] (环入口在索引1)
    ListNode node1 = new ListNode(3);
    ListNode node2 = new ListNode(2);
    ListNode node3 = new ListNode(0);
    ListNode node4 = new ListNode(-4);
    node1.next = node2; node2.next = node3; node3.next = node4; node4.next = node2;
    
    // 检测环入口
    ListNode entry = solution.detectCycle(node1);
    System.out.println("环入口值: " + (entry != null ? entry.val : "无环")); // 1
    
    // 构造单节点环 [1] (尾部连自己)
    ListNode node5 = new ListNode(1);
    node5.next = node5;
    entry = solution.detectCycle(node5);
    System.out.println("环入口值: " + entry.val); // 0
    
    // 构造无环链表 [1,2,3]
    ListNode node6 = new ListNode(1);
    node6.next = new ListNode(2);
    node6.next.next = new ListNode(3);
    entry = solution.detectCycle(node6);
    System.out.println("环入口值: " + (entry != null ? entry.val : "无环")); // 无环
}

常见问题

  1. 为什么快指针走2步?走3步行不行?

    • 走2步能保证数学关系成立(( a = (k-1)L + c ))。走3步可能导致无法推导出线性关系。
  2. 如何确保第二轮一定在入口相遇?

    • 根据数学推导 ( a = (k-1)L + c ),当快指针走 ( a ) 步时,慢指针走 ( a ) 步相当于从相遇点走 ( c ) 步加 ( (k-1) ) 圈,正好到达入口。
  3. 若环很大或很小是否影响结果?

    • 不影响。数学关系 ( a = (k-1)L + c ) 与环大小无关。
  4. 快指针能否初始化为 head.next

    • 可以,但需调整:
      ListNode slow = head;
      ListNode fast = head.next;
      while (fast != slow) {
          if (fast == null || fast.next == null) return null;
          slow = slow.next;
          fast = fast.next.next;
      }
      // 后续相同
      
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值