借助虚拟头节点,解决力扣算法“合并两个有序链表”
题目:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
提示:
-
两个链表的节点数目范围是 [0, 50]
-
-100 <= Node.val <= 100
-
l1 和 l2 均按 非递减顺序 排列

核心思想:
要将两个升序链表合并为一个新的升序链表,可以通过比较两个链表的当前节点,将较小的节点接到新链表上。当一个链表遍历完毕后,将另一个链表直接接到新链表的末尾。最后,返回新链表的头节点。
算法步骤:
-
创建一个虚拟头节点 dummyHead,用于简化边界情况的处理。
-
初始化一个当前节点 res 指向 dummyHead,用于构建新链表。
-
比较两个链表的当前节点,将较小的节点接到新链表上,并将相应的链表指针前移一位。
-
重复步骤3,直到一个链表遍历完毕。
-
将未遍历完的链表直接接到新链表的末尾。
-
返回 dummyHead.next,即新链表的头节点。
针对我给的算法步骤,可能有人会疑惑,为什么要创建一个虚拟头节点呢?
因为创建一个虚拟头节点(也称为哑节点或虚拟节点)在合并链表的算法中有很多好处:
-
简化操作:使用虚拟头节点可以简化对新链表头节点的操作。在合并过程中,我们不需要单独处理新链表头节点的初始化,因为虚拟头节点的下一个节点自然就是新链表的头节点。
-
统一处理:虚拟头节点使得对链表的头部和非头部节点的处理统一。在合并过程中,我们不需要对特殊情况(如其中一个链表为空)进行额外的判断和处理。
-
避免空指针:如果两个输入链表中有一个是空的,使用虚拟头节点可以避免在合并过程中出现空指针异常。因为虚拟头节点始终存在,我们可以安全地返回 dummyHead.next 作为结果。
-
代码清晰:虚拟头节点使得算法的逻辑更加清晰和易于理解。它隐藏了链表操作中一些复杂的边界情况,使得代码更加简洁和直观。
-
兼容性:在一些链表操作中,例如反转链表,虚拟头节点可以提供与非空链表相同的操作接口,使得算法可以适用于更广泛的情况。
而在这个题目中,虚拟头节点 dummyHead 在函数内部创建,不会返回给调用者。它仅用于辅助构建新链表,最终返回的是 dummyHead.next,即新链表的实际头节点。这样做可以避免在函数外部处理边界情况,使函数的接口更加简洁。
复杂度分析:
-
时间复杂度是 O (m + n),其中 m 和 n 分别是两个链表的长度,因为我们最多遍历每个链表一次。
-
空间复杂度是 O (1),因为我们只使用了常数级别的额外空间。
我的 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 list1, ListNode list2) {
// 特殊情况:两个链表都是空链表
if(list1==null && list2==null){
return null;
}
// 创建一个虚拟头节点
ListNode dummyHead = new ListNode(0);
ListNode res = dummyHead;
// 遍历两个链表,直到其中一个链表为空
while(list1!=null && list2!=null){
if(list1.val < list2.val){
res.next = list1;
list1 = list1.next;
}else{
res.next = list2;
list2 = list2.next;
}
res = res.next; // 注意!每次加入新结点后都要向后移动一个位置
}
// 将未遍历完的链表直接接到新链表的末尾
if(list1 == null){
res.next = list2;
}else{
res.next = list1;
}
// 返回新链表的头节点
return dummyHead.next;
}
}

浙公网安备 33010602011771号