LeetCode Hot 100 链表专题2 3 题 (Java)

本文包含 LeetCode 141、142、21 三道链表题,每题统一按 ① 题目分析与解题思路 → ② 常见解法(附代码)及讲解 → ③ 知识点总结 组织。

题号 题目 首选解法 时间 / 空间 与前后题的关系
141 环形链表 快慢指针(Floyd 判圈) O(n) / O(1) 入门:只判断有没有环
142 环形链表 II 快慢指针 + 二次相遇 O(n) / O(1) 141 的延伸:求环入口
21 合并两个有序链表 迭代 + 哑节点(dummy) O(m+n) / O(1) 归并排序 148 的基础

一、141. 环形链表

① 题目分析与解题思路

题目 :给定链表头结点,判断链表是否存在环。有环返回 true,无环返回 false。

环的定义 :某个节点的 next 指针一直走下去,能再次回到自己;题目说明里的 pos 只用于标注环的位置,不是传入参数。

核心思路(进阶要求 O(1) 空间:快慢指针,即 Floyd 判圈算法 / 龟兔赛跑)

  • 慢指针 slow:一次走 1 步;快指针 fast:一次走 2 步。
  • 链表没有环 :fast 会先走到 null,直接返回 false。
  • 链表有环 :快慢指针最终一定会在环内相遇(fast 绕圈追上 slow),相遇则返回 true。

原理 :进入环之后,fast 相对于 slow 每步缩小 1 个节点的距离,距离逐格递减、不存在"跳过",所以一定能碰上,不会永远错开。

② 常见解法(附代码)及讲解

解法 1:快慢指针(推荐,O(n) 时间,O(1) 空间,满足进阶)

java 复制代码
/**
 * 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) {
        ListNode slow = head;
        ListNode fast = head;
        // 只要快指针还能走两步
        while(fast != null && fast.next != null){
            slow = slow.next;       // 慢走1步
            fast = fast.next.next;  // 快走2步
            if(slow == fast){
                return true; // 相遇,存在环
            }
        }
        return false; // 快指针走到末尾,无环
    }
}

✅ 讲解:

  • 循环条件 fast != null && fast.next != null 是防 NPE 的关键:fast 一次走两步,若 fast.next 已是 null,再取 fast.next.next 会空指针异常。
  • 只要能相遇,一定有环;若退出循环,说明走到链尾,无环。
  • 时间复杂度 O(n):无环时遍历一遍;有环时最多遍历一圈多一点(详见 142 题 ③ 的复杂度分析)。

解法 2:哈希集合(HashSet,O(n) 时间,O(n) 空间,好理解,不满足进阶)

思路:遍历链表,把访问过的节点存入集合;每次先判断当前节点是否已经在集合里,存在说明重复访问 → 有环。

java 复制代码
public class Solution {
    public boolean hasCycle(ListNode head) {
        Set<ListNode> set = new HashSet<>();
        ListNode cur = head;
        while(cur != null){
            if(set.contains(cur)){
                return true;
            }
            set.add(cur);
            cur = cur.next;
        }
        return false;
    }
}

✅ 讲解:

  • 存的是节点引用对象 ,不是 val!这是重点,值重复不代表节点重复(原因见下方补充说明)。
  • 优点:直观好写;缺点:额外占用内存,面试优先写快慢指针版本。

补充说明:为什么必须存节点引用,而不是 val

链表节点的值完全可以重复 ,所以哈希解法里存的是节点对象(引用地址) ,而不是节点的 val 值。

举个例子:

plain 复制代码
1 → 2 → 2 → 3
        ↑
        └──┘

这里两个节点 val 都是 2,但它们是内存里两个不同的节点。

  • 如果只存 val 判断:会误以为遇到重复值就是环,直接误判 true。
  • 如果存节点引用:只有同一个节点再次被访问,才判定成环,逻辑才正确。

对应到 141 题考点

  1. val 只是节点附带的数据,题目没有任何保证 val 唯一;题目给的提示范围 -10^5 <= Node.val <= 10^5,也允许重复。
  2. 判断环的本质:是否再次走到同一个节点(同一个内存地址),和值无关。
  3. 易错写法复盘:❌ HashSet<Integer> 存 val 去判断;✅ 正确写法是 HashSet<ListNode> 存节点引用。

一句话区分 :val 是节点上存的数据,可重复;节点引用(对象地址) 每个节点天生唯一,不会重复。

③ 知识点总结

  1. 核心结论 :判环的本质是"节点是否被重复访问",与 val 无关;快慢指针是最优解(O(1) 空间)。
  2. 必背模板 :slow = head, fast = head → while (fast != null && fast.next != null) → 先移动,再判断是否相等。
  3. 易错点复盘
    • 循环条件只写 fast != null,漏写 fast.next != null → 空指针异常。
    • 初始位置写成 slow = head, fast = head.next 也能过,但容易逻辑混乱,标准写法都从 head 出发更稳妥。
    • 判断相等写在移动之前(顺序错),会漏掉相遇判断。
    • 边界忽略空链表:head == null 时直接返回 false(模板天然兼容)。
    • 哈希写法忘记存节点对象、只存 val → 重复值导致误判。
  4. 复杂度对比:快慢指针 O(n) / O(1);HashSet O(n) / O(n)。
  5. 延伸:142 题是本题的延伸------相遇之后还要继续求出环的入口节点。

二、142. 环形链表 II

① 题目分析与解题思路

题目 :给定链表头结点,找到链表环的入口节点;无环返回 null,不允许修改链表。

与 141 的区别:141 只需要判断有没有环,这一题要精准找到环开始的那个节点。

核心:Floyd 判圈算法的数学推导。设:

  • 头节点到环入口的距离:aaa
  • 环入口到快慢指针相遇点的距离:bbb
  • 相遇点再走回环入口的距离:ccc,环总长:b+cb + cb+c

慢指针路程 s=a+bs = a + bs=a+b;快指针路程 f=a+b+k(b+c)f = a + b + k(b+c)f=a+b+k(b+c),kkk 是快指针在环里绕的圈数。快指针速度是慢指针的 2 倍,即 f=2sf = 2sf=2s:

a+b+k(b+c)=2(a+b)a=c+(k−1)(b+c) \begin{align} a + b + k(b+c) &= 2(a+b) \\ a &= c + (k-1)(b+c) \end{align} a+b+k(b+c)a=2(a+b)=c+(k−1)(b+c)

✅ 结论公式 :头结点到环入口的距离 = 相遇点到环入口的距离(多绕 k−1k-1k−1 圈环)。

操作步骤:

  1. 快慢指针同起点 head,slow 一次 1 步、fast 一次 2 步,直到相遇;如果 fast 走到 null,无环返回 null。
  2. 相遇之后,把其中一个指针挪回 head,两个指针现在都一次走 1 步。
  3. 两个指针再次相遇时,相遇点就是环入口。

② 常见解法(附代码)及讲解

解法 1:快慢指针(O(n) 时间,O(1) 空间,满足进阶要求,面试首选)

java 复制代码
/**
 * 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) {
        ListNode slow = head;
        ListNode fast = head;
        // 第一阶段:快慢指针找相遇点
        while(fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
            // 找到了相遇点
            if(slow == fast){
                // 第二阶段:一个指针回到头,两个同速前进
                ListNode p = head;
                while(p != slow){
                    p = p.next;
                    slow = slow.next;
                }
                return p;
            }
        }
        // 快指针走到末尾,无环
        return null;
    }
}

讲解:

  • 第一阶段和 141 环形链表几乎一样,目的只是确认有环并且拿到环内相遇点。
  • 第二阶段核心:p 从 head 出发、slow 从相遇点出发,两者同速,一定会在环入口碰头。
  • 时间复杂度 O(n),最多遍历链表常数次;空间 O(1),只用几个指针。

解法 2:哈希集合(HashSet,O(n) 时间,O(n) 空间,好理解,不满足进阶)

思路 :遍历链表,把节点引用存入集合;第一个重复访问到的节点,就是环入口。

java 复制代码
public class Solution {
    public ListNode detectCycle(ListNode head) {
        Set<ListNode> set = new HashSet<>();
        ListNode cur = head;
        while(cur != null){
            if(set.contains(cur)){
                return cur; // 第一次重复访问,就是环入口
            }
            set.add(cur);
            cur = cur.next;
        }
        return null;
    }
}

讲解:

  • 逻辑直白:环入口是链表中第一个被访问两次的节点。
  • 缺点:需要额外集合存节点,空间开销大;适合初学理解,面试优先写快慢指针版本。
  • ⚠️ 重点:存 ListNode 对象,不是 val!(理由同 141 题的补充说明。)

面试追问补充

  1. 为什么两个指针同速走,一定在入口相遇?→ 就是上面那条数学公式推导。
  2. 如果链表环很大,会不会影响结论?→ 不会,公式里 k 可以是任意正整数。
  3. 和 141 题对比:141 只判存在性;142 求入口,是 141 的延伸。

③ 知识点总结

为什么复杂度是 O(n) 而不是 O(n²)

快慢指针整个过程,每个节点最多被访问常数次(2~3 次) ,不是嵌套循环,没有 n2n^2n2 的行为,所以是 O(n)。

分开两段看,设链表总节点数 nnn,无环部分长度 aaa,环长 LLL,n=a+Ln = a + Ln=a+L:

  • 阶段 1(找相遇点:slow 一步,fast 两步) :慢指针一共走了 a+ba + ba+b 步(bbb 是入口到相遇点距离,b<Lb < Lb<L),步数总量 a+b<a+L=na + b < a + L = na+b<a+L=n,即慢指针最多遍历一遍链表 就相遇;快指针速度是 2 倍,走 2(a+b)2(a+b)2(a+b) 步,依然与 nnn 成正比。
    • 误区澄清 :很多人以为"在环里一直绕圈"会变成 O(n²)。但数学上已证明:慢指针在走完第一圈环之前,快指针一定追上它,slow 最多在环内走不到一圈就相遇,不会无限循环、不会多次完整遍历链表。
    • 极端例子:整个链表就是一个环(a=0a=0a=0,入口就是 head),slow 刚进环走 L/2L/2L/2 左右,fast 就追上了,总共步数 O(L)=O(n)O(L) = O(n)O(L)=O(n)。
  • 阶段 2(找环入口:p 从 head、slow 从相遇点,都一步一步走) :需要再走 aaa 步,a≤na \le na≤n,依然是线性。

两阶段加起来总步数上限:(a+b)+a<n+n=2n(a+b) + a < n + n = 2n(a+b)+a<n+n=2n,2n2n2n 仍属于 O(n),常数倍数不影响复杂度。

对比:什么时候才是 O(n²)

O(n²) 一般是两层循环:

java 复制代码
for i from 0~n:
    for j from 0~n:
        ...

比如:对每个节点,都从头再遍历一遍去找它,总操作 n×nn \times nn×n。快慢指针只有两个指针同步向前跑,没有回头重扫全表,不存在内层循环。

再和哈希方案对照:哈希 Set 解法遍历一遍链表、每个节点访问一次,是 O(n);快慢指针只是最多多跑一小段,本质同样是线性扫描。

通俗比喻 :跑道一共 nnn 米,slow 慢跑 1m/s、fast 快跑 2m/s,跑道后半段是环形跑道。快跑的人不会跑 n×nn \times nn×n 米,他追上慢跑者时总共跑的距离也就 2 倍慢跑距离,和总跑道长度成正比。

配套小思考题 :如果我写成"对每一个节点 cur,我都开一个指针从头走到 cur,看会不会碰到 cur 第二次",这就是双层遍历,才是 O(n²)。快慢指针完全不是这个逻辑。

易错点复盘

  1. 第二步速度搞错:很多人忘了二次相遇时两个指针都要一次一步走,还让 fast 一次走两步,直接错。
  2. 空指针风险 :循环条件 fast != null && fast.next != null 漏写,出现空指针。
  3. 边界 case 忽略:链表只有一个节点且无环、环入口就是 head(整个链表是一个环)。
  4. 混淆 141 和 142 :141 相遇直接 return true;142 相遇只是第一步,还要二次遍历找入口。
  5. 数学理解误区 :误以为相遇点就是环入口,这是最常见错误!相遇点只是环内某一点,不一定是入口。
  6. 哈希解法:依旧不能存 val,必须存节点引用,val 可重复。

三、21. 合并两个有序链表

① 题目分析与解题思路

题目 :给定两个升序(非递减)链表 l1、l2,把它们合并成一条新的升序链表;直接复用原来两个链表的节点,不需要新建节点。

关键点:两个链表本身已经有序;最终链表保持从小到大;可以有空链表。

核心思路(迭代法,最常用):

  1. 新建一个哑结点(dummy 哨兵节点),用来当作合并链表的开头,方便统一处理边界,避免单独判断头节点是谁。
  2. 用一个 cur 指针,一开始指向 dummy,负责往后拼接节点。
  3. 循环:只要 l1、l2 都不为空,比较两个当前节点的值,把更小的那个接到 cur.next,然后对应链表指针后移,cur 同步后移。
  4. 当其中一条链表遍历完了,直接把剩下另一条链表的剩余部分接到 cur.next(剩下的本身有序,不用继续比较)。
  5. 最终返回 dummy.next,就是合并后的链表头。

递归思路 :把问题看成子问题------选出当前更小的节点,然后把它的 next 指向剩下两个链表合并的结果。

② 常见解法(附代码)及讲解

解法 1:迭代 + dummy 哑节点(推荐,面试首选)

  • 时间复杂度:O(m+n),m、n 是两条链表长度,每个节点访问一次。
  • 空间复杂度:O(1),只使用几个指针,没有额外开辟链表空间。
java 复制代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        // 哑结点,哨兵
        ListNode dummy = new ListNode(-1);
        ListNode cur = dummy;

        while(l1 != null && l2 != null){
            if(l1.val <= l2.val){
                cur.next = l1;
                l1 = l1.next;
            }else{
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }
        // 接上剩下没走完的链表
        cur.next = l1 == null ? l2 : l1;
        return dummy.next;
    }
}

讲解:

  • dummy 只是占位符,本身不算有效节点,结果从 dummy.next 开始。
  • cur 是尾指针,永远指向新链表的最后一个节点,用来追加节点。
  • 循环退出条件:任意一条链表走完;剩下部分直接接上,因为本身有序。

解法 2:递归写法(简洁,用于理解递归思路)

  • 时间复杂度:O(m+n)。
  • 空间复杂度:O(m+n),递归调用栈深度等于链表总长度。
java 复制代码
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        // 递归终止条件:一条链表空了,直接返回另一条
        if(l1 == null) return l2;
        if(l2 == null) return l1;

        if(l1.val <= l2.val){
            // 选l1当前节点,l1.next 和 l2继续合并
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        }else{
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
}

讲解:

  1. 递归的含义:返回 l1、l2 合并后的有序链表头。
  2. 谁小谁作为当前头,它的 next 交给子递归去合并剩下部分。
  3. 缺点:递归有调用栈开销,如果链表很长会栈溢出;面试优先迭代版本。

补充对比:迭代 vs 递归

  • 迭代:原地拼接,常数空间,工程上更推荐。
  • 递归:代码短、优雅,但是有栈开销,适合理解分治思想;归并排序里合并有序数组 / 链表就是这个逻辑。

③ 知识点总结

  1. 核心结论 :dummy 哨兵节点是链表题的通用技巧------凡是"结果链表的头节点不确定"的场景(合并、删除、分区),都先建 dummy,最后返回 dummy.next。
  2. 易错点复盘
    • 忘记哑结点:头节点处理麻烦,需要写一堆 if 判断谁是第一个节点,很容易漏边界。
    • 比较之后指针移动混乱:接上节点之后忘记移动 l1 / l2,或者忘记移动 cur,造成死循环。
    • 循环结束后没有拼接剩余链表:循环只处理两个都不为空的情况,剩下那条链表直接丢了。
    • 理解错题意去新建节点:题目允许直接复用原有节点,不需要 new ListNode 复制 val,复制会浪费空间。
    • 空链表边界漏测:l1 为空 / l2 为空 / 两者都为空。
    • 判断条件写反:l1.val > l2.val 用错方向,合并出来是降序链表。
  3. 小细节 :比较时用 <= 而非 <,相等时优先取 l1,可保持合并结果的稳定性(与归并排序一致)。
  4. 延伸 :这一题是归并排序合并阶段的基础,后面 148. 排序链表 会用到这个合并逻辑。

附:三题速记

题号 关键判据 必背要点
141 快慢指针能否相遇 while (fast != null && fast.next != null),先移动再比较
142 相遇后一个指针回 head,同速再走 牢记 a=c+(k−1)(b+c)a = c + (k-1)(b+c)a=c+(k−1)(b+c);相遇点 ≠ 环入口
21 双指针归并 + dummy 收尾 cur.next = l1 == null ? l2 : l1
相关推荐
asaotomo2 小时前
截图识题、AI 讲解、笔记沉淀:Chrome 扩展体验
前端·chrome·笔记
文丑颜不良啊2 小时前
LeetCode1341~1360题目解析
java·leetcode
熊猫钓鱼>_>2 小时前
鸿蒙PC高分屏与键鼠交互适配:DPI缩放、右键菜单一次讲清
开发语言·笔记·华为·ai编程·harmonyos·鸿蒙·鸿蒙心迹
H_BB2 小时前
面试笔记:数据加密和心跳机制
笔记·面试·职场和发展
QianhangQianping2 小时前
撤销重做不是「存快照」:一个可视化编辑器的Operation 设计
java·前端·数据库·typescript·编辑器·前端架构·撤销重做
海绵宝宝转agent2 小时前
LeetCodehot100题解
java·笔记·学习·算法·leetcode·深度优先
SunnyDays10112 小时前
Java 操作 Word 脚注和尾注:添加、读取与删除
java·word
SunnyDays10112 小时前
Java 为 Word 添加页码:隐藏封面页码、从指定页开始编号、奇偶页页码设置等
java·word·页码
开源推荐官2 小时前
多门店 O2O 商城选型复盘:从开发视角对比主流开源系统
java·架构·开源·php