合并两个有序链表
问题
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
输入:l1 = [], l2 = []
输出:[]
输入:l1 = [], l2 = [0]
输出:[0]
前置知识:链表节点定义
ListNode.ts
class ListNode {
val: number;
next: ListNode | null;
constructor(val: number = 0, next: ListNode | null = null) {
this.val = val;
this.next = next;
}
}
什么是链表?
链表是一种线性数据结构,每个节点包含数据和指向下一个节点的指针。与数组不同,链表的元素在内存中不是连续存储的,插入/删除效率高但随机访问效率低。
答案
方法一:迭代(推荐)
核心思路:创建一个虚拟头节点(dummy),用指针逐步比较两个链表的节点值,将较小的接到结果链表后面。
mergeTwoListsIterative.ts
function mergeTwoLists(
list1: ListNode | null,
list2: ListNode | null
): ListNode | null {
// 虚拟头节点,简化边界处理
const dummy = new ListNode(-1);
let current = dummy;
// 两个链表都不为空时,逐个比较
while (list1 !== null && list2 !== null) {
if (list1.val <= list2.val) {
current.next = list1;
list1 = list1.next;
} else {
current.next = list2;
list2 = list2.next;
}
current = current.next;
}
// 将剩余部分接上(只会有一个链表有剩余)
current.next = list1 ?? list2;
return dummy.next;
}
执行过程演示:
l1: 1 → 2 → 4
l2: 1 → 3 → 4
dummy →
Step 1: l1.val(1) <= l2.val(1) → 接 l1(1)
dummy → 1 → l1 移到 2,l2 还在 1
Step 2: l1.val(2) > l2.val(1) → 接 l2(1)
dummy → 1 → 1 → l1 还在 2,l2 移到 3
Step 3: l1.val(2) < l2.val(3) → 接 l1(2)
dummy → 1 → 1 → 2 → l1 移到 4,l2 还在 3
Step 4: l1.val(4) > l2.val(3) → 接 l2(3)
dummy → 1 → 1 → 2 → 3 → l1 还在 4,l2 移到 4
Step 5: l1.val(4) <= l2.val(4) → 接 l1(4)
dummy → 1 → 1 → 2 → 3 → 4 → l1 为 null,l2 还在 4
循环结束,l1 为 null,接上 l2 剩余:
dummy → 1 → 1 → 2 → 3 → 4 → 4
返回 dummy.next
复杂度分析
- 时间复杂度: — m、n 分别是两个链表的长度
- 空间复杂度: — 只使用常数额外空间(不算输出)
dummy 节点技巧
虚拟头节点(哨兵节点)是链表题的常用技巧,可以避免处理"头节点为空"的特殊情况,简化代码逻辑。
方法二:递归
核心思路:递归地比较两个链表的头节点,较小的节点的 next 指向剩余部分的合并结果。
mergeTwoListsRecursive.ts
function mergeTwoLists(
list1: ListNode | null,
list2: ListNode | null
): ListNode | null {
// 递归终止条件:某个链表为空
if (list1 === null) return list2;
if (list2 === null) return list1;
if (list1.val <= list2.val) {
// list1 较小,它的 next 指向剩余部分的合并结果
list1.next = mergeTwoLists(list1.next, list2);
return list1;
} else {
list2.next = mergeTwoLists(list1, list2.next);
return list2;
}
}
递归展开:
merge([1,2,4], [1,3,4])
= 1 → merge([2,4], [1,3,4]) // l1.val(1) <= l2.val(1),取 l1
= 1 → 1 → merge([2,4], [3,4]) // l1.val(2) > l2.val(1),取 l2
= 1 → 1 → 2 → merge([4], [3,4]) // l1.val(2) < l2.val(3),取 l1
= 1 → 1 → 2 → 3 → merge([4], [4]) // l1.val(4) > l2.val(3),取 l2
= 1 → 1 → 2 → 3 → 4 → merge([], [4]) // l1.val(4) <= l2.val(4),取 l1
= 1 → 1 → 2 → 3 → 4 → 4 // l1 为 null,返回 l2
复杂度分析
- 时间复杂度:
- 空间复杂度: — 递归调用栈的深度
递归方法虽然优雅,但会占用额外的栈空间,链表很长时可能导致栈溢出。面试中建议先写迭代法,再提递归法作为补充。
方法三:转数组排序再构建链表(不推荐)
先把两个链表的值提取到数组,排序后重新构建链表:
mergeTwoListsArray.ts
function mergeTwoLists(
list1: ListNode | null,
list2: ListNode | null
): ListNode | null {
const values: number[] = [];
// 收集所有值
let p = list1;
while (p) { values.push(p.val); p = p.next; }
p = list2;
while (p) { values.push(p.val); p = p.next; }
// 排序
values.sort((a, b) => a - b);
// 构建新链表
const dummy = new ListNode(-1);
let current = dummy;
for (const val of values) {
current.next = new ListNode(val);
current = current.next;
}
return dummy.next;
}
不推荐
- 时间复杂度: — 因为额外排序
- 空间复杂度: — 额外数组
- 没有利用已排序的特性,效率较低
常见面试追问
Q1: 如何合并 K 个有序链表?
答案:这是 LeetCode 23,有三种经典方法:
// 方法:分治合并(最优)
function mergeKLists(lists: (ListNode | null)[]): ListNode | null {
if (lists.length === 0) return null;
if (lists.length === 1) return lists[0];
const mid = Math.floor(lists.length / 2);
const left = mergeKLists(lists.slice(0, mid));
const right = mergeKLists(lists.slice(mid));
return mergeTwoLists(left, right);
}
// 时间复杂度:O(N log k),N 是所有节点总数,k 是链表数量
Q2: dummy 节点有什么作用?能不能不用?
答案:可以不用,但代码会更复杂,需要单独处理头节点:
function mergeTwoListsNoDummy(
list1: ListNode | null,
list2: ListNode | null
): ListNode | null {
if (!list1) return list2;
if (!list2) return list1;
let head: ListNode;
if (list1.val <= list2.val) {
head = list1;
list1 = list1.next;
} else {
head = list2;
list2 = list2.next;
}
let current = head;
while (list1 && list2) {
if (list1.val <= list2.val) {
current.next = list1;
list1 = list1.next;
} else {
current.next = list2;
list2 = list2.next;
}
current = current.next;
}
current.next = list1 ?? list2;
return head;
}
Q3: 这题有什么实际应用?
答案:
- 归并排序:核心步骤就是合并两个有序序列
- 数据库查询:合并两个有序的查询结果集(Merge Join)
- 日志合并:多个日志文件按时间戳合并