一、问题理解
问题描述
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例
示例 1:
text
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
text
输入:l1 = [], l2 = []
输出:[]
示例 3:
text
输入:l1 = [], l2 = [0]
输出:[0]
要求
-
时间复杂度: O(n + m),其中 n 和 m 分别为两个链表的长度。
-
空间复杂度: 迭代法 O(1),递归法 O(n + m)(递归调用栈空间)。
-
新链表通过拼接原节点得到,不创建新节点。
二、核心思路:比较与连接
基本思想
合并两个有序链表与合并两个有序数组类似,核心是不断比较两个链表当前节点的值,将较小值的节点接到结果链表的末尾,直到其中一个链表遍历完,然后将另一个链表的剩余部分直接接上。
有两种主流实现方式:
-
迭代法:使用一个哨兵节点(dummy)简化边界处理,通过循环逐步连接节点。
-
递归法:每次比较两个头节点,将较小值的节点作为结果头,然后递归地合并剩余部分。
三、代码逐行解析
方法一:迭代法(最优解,O(1) 空间)
核心思想
-
创建一个虚拟头节点
dummy,用于简化头节点的处理。 -
使用指针
prev指向当前结果链表的末尾。 -
循环比较
l1和l2当前节点的值,将较小者接到prev后面,并移动相应指针。 -
循环结束后,将非空链表的剩余部分直接接到
prev后面。 -
返回
dummy.next作为合并后的头节点。
Python 解法
python
# Definition for singly-linked list.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
# 创建一个虚拟头节点,它的next最终指向合并后的链表头
dummy = ListNode()
prev = dummy # prev 始终指向结果链表的最后一个节点
# 当两个链表都不为空时,比较并连接
while l1 and l2:
if l1.val <= l2.val:
prev.next = l1
l1 = l1.next
else:
prev.next = l2
l2 = l2.next
prev = prev.next # prev 向后移动
# 连接剩余部分(l1 或 l2 可能还有剩余)
prev.next = l1 if l1 else l2
return dummy.next
Java 解法
java
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 创建虚拟头节点
ListNode dummy = new ListNode();
ListNode prev = dummy; // prev 指向结果链表的尾节点
// 当两个链表都不为空时
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
prev.next = l1;
l1 = l1.next;
} else {
prev.next = l2;
l2 = l2.next;
}
prev = prev.next;
}
// 连接剩余部分
prev.next = (l1 != null) ? l1 : l2;
return dummy.next;
}
}
方法二:递归法(代码简洁,但空间复杂度较高)
核心思想
-
递归函数
mergeTwoLists接收两个链表头l1和l2,返回合并后的头节点。 -
基准条件:如果其中一个链表为空,直接返回另一个链表。
-
比较两个头节点的值,将较小值的节点作为合并后的头节点,然后将其
next指向递归合并剩余部分的结果。
Python 解法
python
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
# 基准条件:如果有一个链表为空,返回另一个
if not l1:
return l2
if not l2:
return l1
# 比较头节点值,较小者作为头,并递归合并剩余部分
if l1.val <= l2.val:
l1.next = self.mergeTwoLists(l1.next, l2)
return l1
else:
l2.next = self.mergeTwoLists(l1, l2.next)
return l2
Java 解法
java
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 基准条件
if (l1 == null) return l2;
if (l2 == null) return l1;
// 比较头节点值
if (l1.val <= l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
四、Java 与 Python 语法对比
| 操作 | Java | Python |
|---|---|---|
| 链表节点定义 | public class ListNode { int val; ListNode next; ListNode() {} ... } |
class ListNode: def __init__(self, val=0, next=None): |
| 创建节点 | new ListNode(val) |
ListNode(val) |
| 空值判断 | node == null |
node is None |
| 条件判断 | if (l1 != null && l2 != null) |
if l1 and l2: |
| 三目运算符 | (l1 != null) ? l1 : l2 |
l1 if l1 else l2(或 l1 or l2) |
| 递归调用 | mergeTwoLists(l1.next, l2) |
self.mergeTwoLists(l1.next, l2) |
五、实例演示
示例:l1 = [1,2,4],l2 = [1,3,4]
迭代法过程
| 步骤 | l1 当前 | l2 当前 | 比较 | 操作 | 结果链表(prev 指向末尾) |
|---|---|---|---|---|---|
| 初始 | 1 | 1 | - | dummy -> null | dummy |
| 1 | 1 | 1 | 1 <= 1 | 接 l1 的 1 | dummy -> 1 |
| 2 | 2 | 1 | 1 < 2 | 接 l2 的 1 | dummy -> 1 -> 1 |
| 3 | 2 | 3 | 2 < 3 | 接 l1 的 2 | dummy -> 1 -> 1 -> 2 |
| 4 | 4 | 3 | 3 < 4 | 接 l2 的 3 | dummy -> 1 -> 1 -> 2 -> 3 |
| 5 | 4 | 4 | 4 <= 4 | 接 l1 的 4 | dummy -> 1 -> 1 -> 2 -> 3 -> 4 |
| 6 | null | 4 | l1 空 | 接剩余 l2 的 4 | dummy -> 1 -> 1 -> 2 -> 3 -> 4 -> 4 |
最终结果:[1,1,2,3,4,4]
递归法过程
递归调用栈:
text
merge(1,1) → 比较 1 和 1,取 l1 的 1,然后 merge(2,1)
→ merge(2,1) → 比较 2 和 1,取 l2 的 1,然后 merge(2,3)
→ merge(2,3) → 比较 2 和 3,取 l1 的 2,然后 merge(4,3)
→ merge(4,3) → 比较 4 和 3,取 l2 的 3,然后 merge(4,4)
→ merge(4,4) → 比较 4 和 4,取 l1 的 4,然后 merge(null,4)
→ merge(null,4) → 返回 4
→ 返回 4 → 4.next = 4,返回 4
→ 返回 3 → 3.next = 4,返回 3
→ 返回 2 → 2.next = 3,返回 2
→ 返回 1 → 1.next = 2,返回 1
→ 返回 1 → 1.next = 1,返回 1
最终得到链表:1 → 1 → 2 → 3 → 4 → 4 → null
六、关键细节解析
1. 为什么迭代法需要 dummy 节点?
如果没有 dummy 节点,我们需要单独处理合并后头节点为空的情况(即第一个节点谁来接)。dummy 节点作为哨兵,使得所有节点的操作统一,最后返回 dummy.next 即可,避免了对头节点的特殊处理。
2. 迭代法中,为什么最后可以直接 prev.next = l1 if l1 else l2?
因为 l1 和 l2 中至少有一个为 null,而另一个可能还有剩余节点。由于原链表已经有序,剩余部分整体大于等于已合并部分,直接接上即可。
3. 递归法的基准条件为什么是 if not l1: return l2 和 if not l2: return l1?
这处理了空链表的情况。如果其中一个链表为空,合并结果就是另一个链表。这也是递归的终止条件,避免了无限递归。
4. 递归法中,如何保证不丢失节点?
递归调用 mergeTwoLists(l1.next, l2) 会返回合并后的链表头,将其赋值给 l1.next,这样 l1 就连接上了后续合并的结果。由于 l1 本身被返回作为当前层的结果头,整个链表的连接是完整的。
5. 如何处理两个链表都为空的情况?
迭代法中,while 循环不执行,prev.next = l1 if l1 else l2 中 l1 和 l2 均为 null,所以 prev.next = null,最终返回 dummy.next 也为 null,正确。递归法中,第一个基准条件会触发,例如 if not l1: return l2,此时 l1 为 null,l2 也为 null,返回 null。
七、复杂度分析
迭代法
-
时间复杂度: O(n + m),其中 n 和 m 分别为两个链表的长度。每个节点被访问一次。
-
空间复杂度: O(1),只使用了常数个指针变量。
递归法
-
时间复杂度: O(n + m),每个节点被递归调用一次。
-
空间复杂度: O(n + m),递归调用栈的深度等于合并后链表的长度,在最坏情况下为 n + m。
八、其他解法
解法三:原地合并(不使用 dummy,单独处理头节点)
可以不用 dummy 节点,但需要先确定合并后的头节点,然后循环处理后续节点。代码稍显繁琐,但原理相同。
python
def mergeTwoLists(self, l1, l2):
if not l1 or not l2:
return l1 or l2
# 确定头节点
if l1.val <= l2.val:
head = l1
l1 = l1.next
else:
head = l2
l2 = l2.next
curr = head
while l1 and l2:
if l1.val <= l2.val:
curr.next = l1
l1 = l1.next
else:
curr.next = l2
l2 = l2.next
curr = curr.next
curr.next = l1 or l2
return head
九、常见问题与解答
Q1: 迭代法中,为什么使用 dummy 而不是直接操作 head?
A1: dummy 简化了头节点的处理。如果不使用 dummy,需要先判断哪个链表的头节点更小,作为结果头节点,然后在循环中处理,代码会多出一些条件分支。dummy 让所有节点的操作一致,代码更简洁。
Q2: 递归法会不会导致栈溢出?
A2: 如果链表非常长(例如上万节点),递归深度可能过大,导致栈溢出。在实际应用中,迭代法更安全。但 LeetCode 上通常不会出现这种情况。
Q3: 如果两个链表中有重复值,如何保证稳定性?
A3: 题目没有要求稳定性(即相等元素的相对顺序)。但我们的代码中,当 l1.val <= l2.val 时选择 l1,这保持了 l1 中相等元素在前,但通常合并两个有序链表不需要考虑稳定性。
Q4: 能否修改原链表的结构?
A4: 题目允许通过拼接原节点来创建新链表,因此可以修改原链表的 next 指针,但不创建新节点。
Q5: 如何测试代码?
A5: 可以测试以下情况:
-
两个链表都为空
-
一个为空,另一个非空
-
两个长度相等,值交错
-
一个链表的所有值都小于另一个
-
包含重复值
十、相关题目
1. LeetCode 23. 合并K个升序链表
题目: 合并 k 个有序链表。可以使用分治法或优先队列,本题是它的基础。
2. LeetCode 88. 合并两个有序数组
题目: 合并两个有序数组到第一个数组中。思路类似,但需要从后往前合并以避免覆盖。
3. LeetCode 148. 排序链表
题目: 对链表进行排序,可以用归并排序,其中合并两个有序链表是核心步骤。
4. LeetCode 2. 两数相加
题目: 两个链表表示的数字相加,也涉及链表遍历和连接。
十一、总结
核心要点
-
问题本质: 将两个有序链表合并成一个有序链表,不创建新节点。
-
两种主流解法:
-
迭代法:使用 dummy 节点,时间复杂度 O(n+m),空间 O(1)。
-
递归法:代码简洁,但空间复杂度 O(n+m)。
-
-
关键操作: 比较节点值,将较小者接到结果链表末尾,移动指针。
算法步骤(迭代法)
-
创建
dummy节点,prev指向它。 -
当
l1和l2都不为空时:-
比较
l1.val和l2.val,将较小者接到prev.next。 -
移动较小者所在链表的指针,以及
prev。
-
-
将剩余的非空链表接到
prev.next。 -
返回
dummy.next。
复杂度对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 迭代法 | O(n+m) | O(1) | 空间最优,无栈溢出风险 | 代码稍长 |
| 递归法 | O(n+m) | O(n+m) | 代码简洁优雅 | 可能栈溢出 |
扩展思考
合并两个有序链表是许多复杂链表问题的基础,如归并排序、合并K个链表等。掌握迭代和递归两种写法,有助于灵活应对不同场景。特别是 dummy 节点的使用,是链表操作中简化边界处理的常用技巧。