1. 题目
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
1 2 3 4 5
| 输入: 1 -> 2 -> 4 , 1 -> 3 -> 4 输出: 1 -> 1 -> 2 -> 3 -> 4 -> 4
输入: l1 = [], l2 = [] 输出: []
|
节点定义:
1 2 3 4 5 6 7 8
| class ListNode { val: number; next: ListNode | null; constructor(val?: number, next?: ListNode | null) { this.val = val === undefined ? 0 : val; this.next = next === undefined ? null : next; } }
|
2. 解题思路
经典的**归并(merge)**操作,也是归并排序的核心子步骤。
引入一个哑节点 dummy 作为结果链表的哨兵头,避免讨论「第一个节点是谁」的边界。再用 curr 指向结果链表尾部:
- 同时遍历
l1、l2,比较当前值,把较小的接到 curr.next,对应链表指针前移。
curr 也前移一位。
- 有一方走完,把另一方剩余部分整段接上(它本身已有序)。
- 返回
dummy.next。
- 时间复杂度:
O(m + n),每个节点处理一次。
- 空间复杂度:
O(1),只重排指针,不新建节点。
3. TypeScript 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| function mergeTwoLists( l1: ListNode | null, l2: ListNode | null ): ListNode | null { const dummy = new ListNode(-1); let curr = dummy;
while (l1 !== null && l2 !== null) { if (l1.val <= l2.val) { curr.next = l1; l1 = l1.next; } else { curr.next = l2; l2 = l2.next; } curr = curr.next; }
curr.next = l1 !== null ? l1 : l2;
return dummy.next; }
function mergeTwoListsRecursive( l1: ListNode | null, l2: ListNode | null ): ListNode | null { if (l1 === null) return l2; if (l2 === null) return l1;
if (l1.val <= l2.val) { l1.next = mergeTwoListsRecursive(l1.next, l2); return l1; } else { l2.next = mergeTwoListsRecursive(l1, l2.next); return l2; } }
|
4. 面试延伸
- 合并 K 个有序链表(LeetCode 23) 是本题升级,可用「最小堆」或「分治两两合并」,复杂度
O(N log k)。
dummy 哑节点是链表题的通用技巧,几乎能消除所有头节点特判,务必形成肌肉记忆。
- 稳定性:比较时用
<= 保证相等时优先取 l1,这正是归并排序「稳定」的来源。
难度:简单 | LeetCode 21 题 | 归并思想入门