Appearance
合并两个有序链表
一、题目(LeetCode 21 · 简单 · Hot100)
将两个升序链表合并为一个新的升序链表并返回。新链表由拼接给定的两个链表的所有节点组成。
输入:l1 = 1 -> 2 -> 4, l2 = 1 -> 3 -> 4
输出:1 -> 1 -> 2 -> 3 -> 4 -> 4二、思路演进
2.1 暴力:扔掉"已有序"这个前置条件
把两条链的值全收进数组,排序后重建链表——O((m+n) log(m+n))。这比应有效果多付一个 log 因子,因为**"两条各自有序"的信息被完全浪费**。另一个朴素写法是"每次找较小的头插到结果尾部,但每次从头找尾"——没记住 tail,退化 O(n²)。
2.2 关键洞察:哑结点 + tail,归并逐段前移
有序性保证:结果的下一个节点必然是两条链当前头部中较小的那个。于是:
dummy哑结点固定结果头部(空头),tail记录结果当前的尾;- 两指针
l1、l2指向各链剩余部分的头,取小者接到tail后,该指针前移; - 一条链耗尽后,另一条剩余链整条接上(已有序,无需再动)。
每个节点只被访问一次,"已有序"被利用到极致。
2.3 与数组版的镜像对比
库内旧文 合并两个有序数组 是同一思想的数组版,方向却相反。数组从后往前:尾部有预留空间,且后向前填充避免整体挪动。链表从前往后:插入是 O(1),根本没有挪动成本。方向选择由存储结构决定——这是面试讲双指针时的加分点。
三、复杂度分析
- 时间 O(m + n):每轮循环淘汰一个节点,共 m + n 轮,每轮 O(1)。
- 空间 O(1):只重排已有节点,不新建(递归版除外)。
四、代码
java
package org.example;
/**
* @author MUYI
* @version 1.0
* @date 2026/10/17
*/
public class LeetCode_21 {
public static void main(String[] args) {
ListNode l1 = build(new int[]{1, 2, 4});
ListNode l2 = build(new int[]{1, 3, 4});
ListNode node = mergeTwoLists(l1, l2);
while (node != null) { // 期望输出 1 1 2 3 4 4
System.out.print(node.val + " ");
node = node.next;
}
}
/** 迭代版:哑结点定头、tail 记尾、两指针取小者接上 */
public static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0); // 哑结点:省掉"结果头是谁"的特判
ListNode tail = dummy;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) { // 相等取 l1,保持稳定
tail.next = l1;
l1 = l1.next;
} else {
tail.next = l2;
l2 = l2.next;
}
tail = tail.next;
}
// 剩余的那条链整条接上,而不是逐个复制
tail.next = (l1 != null) ? l1 : l2;
return dummy.next;
}
/** 递归版:写法最短,但长链栈深 */
public static ListNode mergeRecursive(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val <= l2.val) {
l1.next = mergeRecursive(l1.next, l2);
return l1;
}
l2.next = mergeRecursive(l1, l2.next);
return l2;
}
static class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
}
static ListNode build(int[] values) {
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
for (int v : values) {
tail.next = new ListNode(v);
tail = tail.next;
}
return dummy.next;
}
}五、坑点
- 不用哑结点就要为"结果的第一个节点来自哪条链"写 if/else 特判,且循环里还得判结果链是否为空。代价:逻辑翻倍、错一处的概率翻倍。
- 循环后忘记接剩余链:剩的那条已有序,
tail.next = 剩余头一步完成,逐个复制是画蛇添足。 - 递归版在长链上栈溢出:10^4 ~ 10^5 节点量级就会触及默认栈深,生产代码选迭代。同族递归陷阱见 反转链表。
- 比较方向:升序输入取小者;有人手滑写成取大者,示例对但排序方向反。
- 相等时取 l1 还是 l2 结果值相同,取 l1 保持归并稳定性——被追问稳定性时要答得上来。
六、变形追问
- 变体一(合并 K 个升序链表,LeetCode 23,Hot100):两条两两顺序合并是 O(kN)。进阶是小根堆或分治两两合并。小根堆:堆里放 k 条链当前头,每轮弹最小接上再压它的 next,O(N log k)。分治两两合并:同复杂度,且可以复用本题函数。
- 变体二(追问):"两条链分别是升序和降序,怎么合并?"——先把降序那条反转链表成升序,再走本模板。考察的是"先归一化输入形态再套用标准过程"的意识。相关练习:排序链表 LeetCode 148,找中点 + 反转 + 归并。
2026-10 新增(E 轨 · Hot100 第一批)。