当前位置: 首页 > news >正文

LeetCode 21. 合并两个有序链表

一、题目与考点拆解

题目要求

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

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

核心考点

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

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

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


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

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

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

整体流程:

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

三、最终定稿 AC 代码

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. 循环拼接主体

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 等于「两条剩余链表的合并结果」。

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. 最后形成条件反射:只要是链表拼接 / 头节点不确定的题,先写哑节点
http://www.jsqmd.com/news/1380516/

相关文章:

  • Local-first Markdown编辑器:离线优先的极致写作体验与数据自主方案
  • 【一个聚合支付平台,持续更新】
  • CAN总线远距离通讯实战:从原理到部署的完整解决方案
  • Linux-----Linux文件系统解读
  • AI多Agent协作系统实战(二十二):从6列到12列——任务监控报告的进化之路
  • 2026洛阳外墙漏水避坑指南 - 伶鹿到家
  • 第50篇-多数据源集成
  • 从CTF到企业实战:构建分层日志分析框架与ELK/SIEM工具链
  • C++形参默认值:语法规则、编译器实现与实战避坑指南
  • VMware ESXi虚拟机CentOS 7系统盘扩容实战:从VMDK到LVM全流程详解
  • 计算机保研预推免笔面试全攻略:从算法真题到项目深挖
  • LeetCode最长连续序列哈希表解法详解
  • 自我认知重构:从系统思维到行为调试的工程化实践
  • Excel专业修约:四舍六入五成双的VBA与公式实现
  • VC6.0工程文件修复工具:解析、诊断与自动修复.dsp/.dsw文件
  • AI编程助手上下文管理:从Token原理到实战解决Claude Code“失忆”问题
  • 【非标自动化】2、认识元器件(固态继电器)
  • CS:S武器手感调优指南:从视图模型到网络参数的全面配置
  • 福州大学控制类考研专业深度解析:学硕、专硕与交叉方向如何选择
  • Agent Harness框架:构建生产级AI Agent的工程化实践指南
  • DeepSpeed ZeRO-3保存Checkpoint后OOM:原理、诊断与解决方案
  • 兼容适配:M-Robots 如何实现 ROS 生态无缝迁移,降低替换成本
  • AI术语解析:从Agent到向量数据库,穿透技术黑话迷雾
  • 芯片测试:从DFT设计到量产良率管理的系统工程实践
  • RCE漏洞挖掘实战:从原理到Payload构造与绕过技巧
  • 利用ShellcodePack实现DLL与COM劫持:高隐蔽性Shellcode加载与持久化技术详解
  • Logo 设计工具记录:多款智能 Logo 生成工具能力边界整理
  • Python读取TIF文件全攻略:从Pillow到rasterio的实战选型
  • Oracle 19c Linux静默安装实战:从系统调优到建库配置全解析
  • 2026模板小程序开发服务商哪家更新快?运维有保障才是真的好!