LeetCode 21. 合并两个有序链表

一、题目与考点拆解

题目要求

将两个升序链表合并为一个新的升序链表并返回,新链表由给定的两个链表的所有节点拼接组成。

  • 输入:两条各自升序排列的单链表
  • 输出:合并后的升序单链表
  • 要求:直接复用原链表节点,不需要新建节点存值

核心考点

这道题真正考察的不是逻辑复杂度,而是链表操作的基本功

  1. 指针的移动与拼接,理解「修改 next 指针就是修改链表结构」
  2. 哑节点(哨兵节点)的使用,统一头节点与中间节点的处理逻辑
  3. 边界处理:空链表、一条先走完的剩余拼接

最优解为双指针迭代法,时间复杂度 O (n+m)(两条链表各遍历一次),空间复杂度 O (1)(仅用常数个指针变量)。


二、核心思路:哑节点 + 双指针迭代

两条链表本身都是升序的,我们只需要用两个指针分别指向两条链表的当前节点,每次选值更小的节点拼接到结果链表尾部即可。

核心技巧是哑节点(Dummy Node):先创建一个无业务值的占位头节点,所有节点都统一拼接到它的后面,彻底消除「第一个节点到底属于哪条链表」的特殊判断,代码逻辑极度简洁。

整体流程:

  1. 创建哑节点作为结果链表的占位头,用一个尾部指针 cur 永远指向已拼接部分的最后一个节点
  2. 同时遍历两条链表,谁的值更小,就把谁的节点接到 cur 后面,对应链表指针后移一步
  3. 每次拼接完成后,cur 同步后移一步,永远保持在结果链表尾部
  4. 当其中一条链表遍历完毕时,直接把另一条链表的剩余部分整条接到尾部
  5. 最终返回哑节点的 next,也就是真正的结果链表头节点

三、最终定稿 AC 代码

java 复制代码
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        // 哑节点:占位用的空头节点,统一拼接逻辑
        ListNode head = new ListNode();
        // 尾部指针:永远指向当前已拼接链表的最后一个节点
        ListNode cur = head;

        // 两条链表都不为空时,两两比较拼接
        while (list1 != null && list2 != null) {
            if (list1.val < list2.val) {
                cur.next = list1;
                list1 = list1.next;
            } else {
                cur.next = list2;
                list2 = list2.next;
            }
            // 尾部指针同步后移
            cur = cur.next;
        }

        // 收尾:谁还有剩余,直接整条接在末尾
        cur.next = list1 != null ? list1 : list2;

        // 哑节点本身无意义,返回真正的头节点
        return head.next;
    }
}

四、逐行深度拆解

1. 初始化:哑节点 + 尾部指针

复制代码
ListNode head = new ListNode();
ListNode cur = head;
  • head 是哑节点,本身不存储有效数据,只起占位作用。 没有哑节点的话,需要单独判断第一个节点来自 list1 还是 list2,单独给结果链表的头赋值,逻辑繁琐且容易出空指针;有了哑节点,所有节点都用 cur.next = xxx 的统一逻辑拼接。
  • cur 是尾部指针,永远指向已经拼好的链表的最后一个节点,负责承接下一个新节点。初始时和哑节点重合,代表还没有拼接任何有效节点。

2. 循环拼接主体

java 复制代码
while (list1 != null && list2 != null) {
    if (list1.val < list2.val) {
        cur.next = list1;
        list1 = list1.next;
    } else {
        cur.next = list2;
        list2 = list2.next;
    }
    cur = cur.next;
}
  • 循环条件必须是 && :只有两条链表都还有节点时,才继续两两比较;只要有一条走完了,就退出循环。 ❌ 如果写成 ||,会出现某条链表已经为空还去取 val 的情况,直接空指针异常。
  • 拼接顺序:先把节点接到 cur 后面,再把对应链表的指针往后挪一步。
  • 最容易漏的一行cur = cur.next 接上新节点后,尾部指针必须同步后移,否则下一次拼接会覆盖掉上一个节点,最终结果只剩最后一个节点。这是链表题最高频的手写错误。

3. 收尾:三目运算符拼接剩余链表

复制代码
cur.next = list1 != null ? list1 : list2;

这是新手最容易疑惑的一行,拆开讲透:

  1. 循环退出的必然结果:因为循环条件是两条都不为空才继续,所以退出时一定是「一条已经空了,另一条还有剩余」,不可能两条同时剩,也不会两条同时空(除非输入本身全空)。

  2. 为什么能直接整条接:剩余的链表本身就是升序的,且所有节点的值都大于等于已经拼好的节点,不需要再遍历比较,直接整条接上即可。这是链表结构的优势 ------ 只改一个 next 指针,就能接上一整段,不需要逐个拷贝。

  3. 语法等价 :这是 Java 三元运算符,和下面的 if-else 完全等价:

    复制代码
    if (list1 != null) {
        cur.next = list1;
    } else {
        cur.next = list2;
    }

    作用就是判断谁还有剩余,就把谁剩下的整条链表接在结果尾部。

4. 返回结果

复制代码
return head.next;

head 是我们自己创建的哑节点,没有业务意义,真正的结果链表从它的下一个节点开始。 ❌ 新手高频坑:直接返回 head,会导致结果多一个无意义的默认值头节点。


五、新手必踩坑清单

坑 1:漏掉 cur = cur.next

  • 现象:所有节点都被覆盖,最终结果只剩最后一个节点
  • 原因:尾部指针没有同步后移,每次拼接都在同一个位置覆盖

坑 2:直接返回 head 而非 head.next

  • 现象:结果链表开头多了一个值为 0 的无效节点
  • 原因:忘记哑节点只是占位用的,真正的头在它的 next

坑 3:循环条件写成 ||

  • 现象:运行时空指针异常
  • 原因:一条链表走完后还继续访问它的 val,必然报错

坑 4:收尾部分还要写循环逐个拼接

  • 现象:逻辑冗余,代码变长
  • 原因:没利用好链表本身的有序性和指针特性,剩余部分整条拼接即可,无需遍历

坑 5:不用哑节点,单独处理头节点

  • 现象:逻辑分支多,边界判断繁琐,极易出错
  • 原因:没掌握哑节点技巧,链表拼接题优先上哑节点是通用最优解

六、拓展:递归版实现

这道题也有非常简洁的递归写法,核心思想完全一致:选出当前更小的节点,它的 next 等于「两条剩余链表的合并结果」。

java 复制代码
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        // 递归终止:一条为空,直接返回另一条
        if (list1 == null) return list2;
        if (list2 == null) return list1;

        if (list1.val < list2.val) {
            list1.next = mergeTwoLists(list1.next, list2);
            return list1;
        } else {
            list2.next = mergeTwoLists(list1, list2.next);
            return list2;
        }
    }
}

递归版代码更短,但递归栈会带来 O (n+m) 的空间开销,面试手写优先推荐迭代版,递归版可作为思路补充。


七、面试相关

口述思路(直接背)

这道题我用迭代法加哑节点来做。首先创建一个哑节点作为结果链表的占位头,用一个指针维护当前尾部,然后同时遍历两条有序链表,每次选择值更小的节点拼接到尾部,对应链表指针后移。当其中一条链表遍历完时,直接把另一条的剩余部分接在尾部,最终返回哑节点的下一个节点。

时间复杂度是 O (n+m),两条链表各遍历一次;空间复杂度 O (1),只用到常数个指针变量。

高频追问

  1. 为什么要用哑节点? 统一头节点和中间节点的处理逻辑,不需要单独判断第一个节点属于哪条链表,简化边界处理,减少空指针风险。
  2. 时间和空间复杂度? 迭代版时间 O (n+m),空间 O (1);递归版时间相同,空间 O (n+m) 递归栈开销。
  3. 这道题是归并排序的哪一步? 对应归并排序的「合并两个有序区间」步骤,只是数组换成了链表,逻辑完全一致。
  4. 合并 k 个有序链表怎么做? 可以用分治思路,两两合并;也可以用优先队列,每次取出值最小的节点。

八、复习速记口诀

哑节点,做排头,尾针跟着节点走; 谁小接谁指针移,剩的整条接后头。


总结

合并两个有序链表是链表题型的基础模板题,核心套路非常固定。这道题最大的价值不是学会合并本身,而是掌握哑节点这个链表题的通用神器 ------ 后续的链表翻转、删除节点、链表分区等题目,都可以用哑节点来简化边界处理。

复习优先级:

  1. 先记核心框架:哑节点 + 尾指针 + 双链表遍历 + 剩余拼接
  2. 再避两个高频坑:别忘移尾针、别返回哑节点本身
  3. 最后形成条件反射:只要是链表拼接 / 头节点不确定的题,先写哑节点
相关推荐
Rabitebla1 小时前
C++11 新特性详解(一):列表初始化、initializer_list 与右值引用
java·开发语言·数据结构·c++·算法·leetcode·list
XMAIPC_Robot1 小时前
RK3588+STM32:高性能机器人运动控制解决方案,兼顾实时性与AI算力
人工智能·stm32·嵌入式硬件·算法·fpga开发·机器人·arm+fpga
天吾cc1 小时前
RTT-MQTT
网络·单片机·嵌入式硬件·算法
vivo互联网技术1 小时前
MagicBokeh:单步统一生成式框架实现真实感长焦虚化渲染 | CVPR 2026 Oral
人工智能·算法
lucas_AI2 小时前
Grok 4.6:追平 GPT-5.6 Sol 的半价旗舰,但别只看跑分
人工智能·算法
zephyr052 小时前
链表实现O(n log n)时间复杂度的排序——归并排序详解
java·数据结构·算法
喜欢睡觉2 小时前
双指针判断子序列
算法
贵慜_Derek2 小时前
vLLM-07|MegaMoE 与 FusedMoE:路由相同,算 expert 完全不同
人工智能·算法·llm
机器学习之心3 小时前
基于改进鲸鱼优化算法的CNN-BiLSTM-MATT短期电力负荷预测模型
人工智能·算法·cnn·cnn-bilstm-matt·短期电力负荷预测