一、题目要求
给你两个 升序排列 的单链表 list1 和 list2,
请你将它们合并成一个新的 升序链表,并返回合并后的链表头节点。
示例:
list1: 1 -> 2 -> 4
list2: 1 -> 3 -> 4
输出: 1 -> 1 -> 2 -> 3 -> 4 -> 4
注意几个关键词:
- 两个链表都是升序
- 合并后仍然要保持升序
- 不能破坏链表结构(只调整指针)
二、常见误区
很多人一上来会想:
- 把所有值取出来,放进数组排序
- 或者新建一堆节点再拼
这些方法虽然能做出来,但完全绕开了链表这道题真正想考的点:
指针如何在两个链表之间移动和衔接
三、核心思路:虚拟头结点 + 双指针
1. 为什么要用“虚拟头结点”
ListNode preHead = new ListNode(-1);
作用只有一个,但非常关键:
- 避免单独处理“第一个节点”
- 让所有节点的拼接逻辑完全一致
你只需要关心:
当前节点该接谁,而不用关心“是不是头节点”
2. 两个指针同时往前走
list1:指向链表 1 当前节点list2:指向链表 2 当前节点preNext:始终指向合并后链表的最后一个节点
四、完整代码
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 虚拟头结点
ListNode preHead = new ListNode(-1);
ListNode preNext = preHead;
// 同时遍历两个链表
while (list1 != null && list2 != null) {
if (list1.val <= list2.val) {
preNext.next = list1;
list1 = list1.next;
} else {
preNext.next = list2;
list2 = list2.next;
}
preNext = preNext.next;
}
// 拼接剩余部分
preNext.next = list1 == null ? list2 : list1;
return preHead.next;
}
}
五、逐行拆解:指针是怎么动的
1. 初始化阶段
ListNode preHead = new ListNode(-1);
ListNode preNext = preHead;
当前结构是:
preHead(-1) -> null
preNext 指向 preHead
此时还没真正开始合并。
2. 主循环条件
while (list1 != null && list2 != null)
含义是:
- 只要两个链表都有节点
- 就可以继续比较、继续合并
3. 比较当前节点值
if (list1.val <= list2.val) {
preNext.next = list1;
list1 = list1.next;
} else {
preNext.next = list2;
list2 = list2.next;
}
这一步在做三件事:
- 选出较小的节点
- 接到合并链表后面
- 被选中的链表指针向前移动
4. 移动合并链表指针
preNext = preNext.next;
这一句非常重要,它保证了:
下一次再接节点时,一定是接在“当前链表的尾部”
5. 拼接剩余链表
preNext.next = list1 == null ? list2 : list1;
为什么可以直接接?
因为:
- 如果
list1先空了,list2剩下的一定是有序的 - 反过来也一样
这里不需要再比较,直接整体挂上即可。
6. 为什么返回 preHead.next
return preHead.next;
原因很简单:
preHead是人为加的虚拟节点- 真正的合并结果从它的下一个节点开始
六、用一个过程图理解
假设:
list1: 1 -> 3 -> 5
list2: 2 -> 4 -> 6
合并过程是:
-1 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6
preNext 每次都只做一件事:
把当前最小的节点接过来,然后自己往后挪一格
七、复杂度分析
-
时间复杂度:
O(n + m)- 每个节点只访问一次
-
空间复杂度:
O(1)- 只用了常数级指针