
一、题目简介
题目 :将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。输入 :两个有序单链表 list1、list2输出:合并完成的有序链表头节点
核心难点
- 链表节点只能单向遍历,不能回退,需要持续维护当前拼接位置;
- 两个链表长度不一致,循环结束后需要拼接剩余未遍历完的节点;
- 边界处理:其中一个链表为空、两个链表都为空;
- 避免处理头节点特殊逻辑,写出简洁统一的代码。
最优解法
虚拟头结点(Dummy Node)迭代法
- 时间复杂度:O(m+n),、 为两个链表长度,每个节点仅遍历一次;
- 空间复杂度:O(1),仅新增常数个指针,无额外数组 / 容器;
- 面试核心考点:虚拟头节点、链表指针移动逻辑。
二、算法核心思路(虚拟头结点思想)
1. 为什么要用虚拟头结点?
如果不使用虚拟节点,需要先判断 list1[0] 和 list2[0] 的最小值作为结果链表的头,后续循环逻辑和头节点判断逻辑割裂,代码会出现大量 if 分支,可读性差。
虚拟头结点 dummy 是一个无业务值的空节点,永远作为固定起点:
- 用指针
cur指向dummy,所有节点统一挂载到cur.next; - 遍历过程中只需要比较当前
list1、list2的节点值,小的节点接在cur后方; - 遍历完成后,
dummy.next就是合并链表真正的头节点,直接返回即可,无需额外判断头部。
2. 完整流程步骤
- 创建虚拟头节点
dummy,定义工作指针cur = dummy; - 循环:当
list1和list2都还有节点时,比较两者节点值,将更小节点挂载到cur.next,同时移动对应链表指针和工作指针cur; - 循环结束后,必有一条链表已经遍历完毕,剩余整条链表直接逐个挂载到
cur后方; - 返回
dummy.next,丢弃无意义的虚拟头节点。
三、你的手写完整代码(逐行拆解)
js
ini
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} list1
* @param {ListNode} list2
* @return {ListNode}
*/
var mergeTwoLists = function(list1, list2) {
// 1. 创建虚拟头节点
const dummy=new ListNode();
// 2. cur 为工作指针,初始指向虚拟头
let cur=dummy;
// 3. 双链表都存在节点时,循环比较合并
while(list1&&list2){
if(list1.val<list2.val){
cur.next=list1;
list1=list1.next
cur=cur.next;
}else {
cur.next=list2;
list2=list2.next;
cur=cur.next;
}
}
// 4. 遍历拼接 list1 剩余节点
while(list1!==null){
cur.next=list1;
list1=list1.next;
cur=cur.next;
}
// 5. 遍历拼接 list2 剩余节点
while(list2!=null) {
cur.next=list2;
list2=list2.next;
cur=cur.next;
}
// 6. 返回虚拟节点的下一个,即真实链表头
return dummy.next;
};
四、逐行深度解析
1. 虚拟头结点初始化
js
ini
const dummy=new ListNode();
let cur=dummy;
dummy:占位节点,dummy.val无实际意义,最终不会出现在结果中;cur:工作游标指针 ,永远指向合并链表的最后一个节点,所有新节点都挂载在cur.next;- 优势:统一所有节点的挂载逻辑,不用单独处理链表头部。
2. 核心合并循环 while(list1&&list2)
循环条件 list1 && list2 含义:只有两个链表当前都存在节点时,才需要比较大小。
js
ini
if(list1.val<list2.val){
cur.next=list1; // 将更小的list1节点接到cur后方
list1=list1.next; // list1指针后移,准备下一轮比较
cur=cur.next; // 工作指针后移,指向新的链表末尾
}else {
cur.next=list2;
list2=list2.next;
cur=cur.next;
}
三步固定操作:
- 把更小节点赋值给
cur.next,完成挂载; - 原链表指针
list1/list2向后走一位; - 工作指针
cur同步后移,保证cur永远在链表尾部。
3. 拼接剩余节点的两段 while 循环
js
vbscript
while(list1!==null){cur.next=list1;list1=list1.next;cur=cur.next;}
while(list2!=null) {cur.next=list2;list2=list2.next;cur=cur.next;}
- 当第一层循环退出时,
list1、list2必有一个为null; - 剩余链表是完整有序链,代码采用逐个遍历挂载的写法,逻辑直观,新手更容易理解链表指针移动;
- 补充简化写法:链表节点自带完整后续链,可直接一行挂载,无需循环:
cur.next = list1 || list2,性能一致,代码更简短。
4. 返回结果 return dummy.next
虚拟头节点只是临时占位,dummy.next 才是合并后链表第一个有业务值的节点。
五、两种剩余节点写法对比
写法 1:你的逐一遍历写法(新手友好,本文代码)
js
ini
while(list1!==null){
cur.next=list1;
list1=list1.next;
cur=cur.next;
}
while(list2!=null) {
cur.next=list2;
list2=list2.next;
cur=cur.next;
}
✅ 优点:完全展示指针移动过程,适合初学链表、理解单链结构;❌ 缺点:代码行数更多,存在重复循环逻辑。
写法 2:一行直接挂载(面试精简标准写法)
js
ini
cur.next = list1 ? list1 : list2;
✅ 优点:一行代码搞定,无循环,简洁高效,工业代码通用;💡 原理:链表节点的 next 自带整条后续链条,只需要挂载头部节点,后面所有节点自动跟随。
精简版完整代码(面试推荐)
js
ini
var mergeTwoLists = function(list1, list2) {
const dummy = new ListNode();
let cur = dummy;
while(list1 && list2) {
if(list1.val < list2.val) {
cur.next = list1;
list1 = list1.next;
} else {
cur.next = list2;
list2 = list2.next;
}
cur = cur.next;
}
// 直接挂载剩余整条链表
cur.next = list1 || list2;
return dummy.next;
};
六、你的代码优缺点分析
✅ 代码优点
- 逻辑分层清晰:主循环合并双链表 + 两次循环补全剩余节点,流程一目了然;
- 指针操作规范 :每次挂载节点后同步移动
list指针和工作指针cur,不会出现断链、死循环; - 虚拟头节点使用标准:完美规避头部节点特殊判断,是链表题型通用模板;
- 无空指针风险 :所有循环都做非空判断,不会读取
null.val、null.next。
⚠️ 可优化点
- 剩余链表无需循环遍历,直接一行挂载即可精简代码;
- 分支内
cur=cur.next重复书写,可提取到if-else外部,减少冗余代码:
js
ini
while(list1&&list2){
if(list1.val<list2.val){
cur.next=list1;
list1=list1.next
}else {
cur.next=list2;
list2=list2.next;
}
cur=cur.next; // 提取到分支外,只写一次
}
七、面试高频问答总结
Q1:虚拟头结点的作用是什么?不使用会有什么问题?
A:统一链表头部、中部、尾部的节点挂载逻辑。不使用虚拟节点时,需要额外写逻辑判断两个链表哪个头节点更小作为结果头,代码分支冗余,边界容易出错。
Q2:时间复杂度为什么是 O (m+n)?
A:两个链表每个节点只会被读取、挂载一次,没有重复遍历;虚拟节点仅占用常数空间,空间复杂度 O (1)。
Q3:递归解法和迭代(dummy)解法怎么选择?
A:递归代码简短,但存在递归调用栈,链表过长会栈溢出;面试优先写迭代虚拟头结点解法,稳定性更好,面试官更认可。
Q4:循环结束后为什么不用拼接两条链表?
A:while(list1 && list2) 循环终止条件是其中一条链表遍历完毕,只存在一条链表有剩余节点,二选一挂载即可。
八、刷题总结
合并两个有序链表是单链表入门必刷模板题,核心掌握两大思想:
- 虚拟头结点 Dummy:解决链表头部特殊处理问题,适用于绝大多数链表增删、合并题型;
- 双指针同步遍历:两个有序结构归并,是归并排序的基础思想;
- 剩余链直接挂载:链表是链式存储,无需逐个节点复制,直接挂载整条剩余链。
你手写的遍历剩余节点写法非常适合新手夯实指针基础,熟练后可改用一行挂载精简代码,两种写法都能通过全部测试用例。