21. Merge Two Sorted Lists 合并两个有序链表:多语言实现与分析
一、题目分析
给定两个有序链表 l1
和 l2
,要求将它们合并成一个新的有序链表。新链表由 l1
和 l2
的节点拼接而成。
二、常用解法
迭代法
- 思路:
- 创建一个新的哑节点(dummy node)作为合并后链表的头节点,这样可以简化处理第一个节点的特殊情况。
- 使用一个指针
cur
来跟踪新链表的当前节点。 - 同时遍历
l1
和l2
,比较当前l1
和l2
节点的值。将值较小的节点接到cur
指针的后面,并将相应链表的指针向前移动一位,然后cur
指针也向前移动一位。 - 当其中一个链表遍历完后,将另一个链表剩余的部分直接接到
cur
指针的后面,因为这两个链表本身就是有序的。 - 最后返回哑节点的下一个节点,即为合并后的有序链表的头节点。
- 优点:迭代法的逻辑直观,易于理解和实现。它通过逐步比较和拼接节点,直接构建合并后的链表,在时间和空间的利用上较为高效。
递归法
- 思路:
- 递归的终止条件是
l1
和l2
都为空,此时返回None
。如果l1
为空,返回l2
;如果l2
为空,返回l1
。 - 比较
l1
和l2
当前节点的值,将值较小的节点作为合并后链表的当前节点。 - 对于值较小的节点的下一个节点,递归地调用
mergeTwoLists
函数,将其与另一个链表剩余部分进行合并,并将结果接到当前节点的后面。 - 返回合并后的链表头节点。
- 递归的终止条件是
- 优点:递归法代码简洁,能够清晰地表达问题的递归结构。它通过不断将问题分解为更小的子问题来解决,在某些情况下更符合人们对问题的思考方式。
三、多语言实现
Python实现
- 迭代法
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def mergeTwoLists(self, l1, l2):
head = ListNode(0)
move = head
if not l1: return l2
if not l2: return l1
while l1 and l2:
if l1.val < l2.val:
move.next = l1
l1 = l1.next
else:
move.next = l2
l2 = l2.next
move = move.next
move.next = l1 if l1 else l2
return head.next
- 递归法
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def mergeTwoLists(self, l1, l2):
if not l1 and not l2:
return None
elif not l1:
return l2
elif not l2:
return l1
if l1.val <= l2.val:
node = l1
node.next = self.mergeTwoLists(l1.next, l2)
else:
node = l2
node.next = self.mergeTwoLists(l1, l2.next)
return node
Java实现
- 迭代法
// Definition for singly-linked list.
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
}
}
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head = new ListNode(0);
ListNode move = head;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
move.next = l1;
l1 = l1.next;
} else {
move.next = l2;
l2 = l2.next;
}
move = move.next;
}
if (l1 != null) {
move.next = l1;
} else {
move.next = l2;
}
return head.next;
}
}
- 递归法
// Definition for singly-linked list.
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
}
}
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null && l2 == null) {
return null;
} else if (l1 == null) {
return l2;
} else if (l2 == null) {
return l1;
}
ListNode node;
if (l1.val <= l2.val) {
node = l1;
node.next = mergeTwoLists(l1.next, l2);
} else {
node = l2;
node.next = mergeTwoLists(l1, l2.next);
}
return node;
}
}
C++实现
- 迭代法
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if (!l1) return l2;
if (!l2) return l1;
ListNode* dummy = new ListNode(0);
ListNode* cur = dummy;
while (l1 && l2) {
if (l1->val < l2->val) {
cur->next = l1;
l1 = l1->next;
} else {
cur->next = l2;
l2 = l2->next;
}
cur = cur->next;
}
if (l1) cur->next = l1;
else cur->next = l2;
return dummy->next;
}
};
- 递归法
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if (!l1 &&!l2) {
return nullptr;
} else if (!l1) {
return l2;
} else if (!l2) {
return l1;
}
ListNode* node;
if (l1->val <= l2->val) {
node = l1;
node->next = mergeTwoLists(l1->next, l2);
} else {
node = l2;
node->next = mergeTwoLists(l1, l2->next);
}
return node;
}
};
Go实现
- 迭代法
package main
import "fmt"
// Definition for singly-linked list.
type ListNode struct {
Val int
Next *ListNode
}
func mergeTwoLists(l1, l2 *ListNode) *ListNode {
dummy := &ListNode{}
cur := dummy
for l1 != nil && l2 != nil {
if l1.Val < l2.Val {
cur.Next = l1
l1 = l1.Next
} else {
cur.Next = l2
l2 = l2.Next
}
cur = cur.Next
}
if l1 != nil {
cur.Next = l1
} else {
cur.Next = l2
}
return dummy.Next
}
- 递归法
package main
import "fmt"
// Definition for singly-linked list.
type ListNode struct {
Val int
Next *ListNode
}
func mergeTwoLists(l1, l2 *ListNode) *ListNode {
if l1 == nil && l2 == nil {
return nil
} else if l1 == nil {
return l2
} else if l2 == nil {
return l1
}
var node *ListNode
if l1.Val <= l2.Val {
node = l1
node.Next = mergeTwoLists(l1.Next, l2)
} else {
node = l2
node.Next = mergeTwoLists(l1, l2.Next)
}
return node
}
四、算法复杂性分析
时间复杂度
- 迭代法和递归法:两种方法都需要遍历
l1
和l2
中的所有节点,因此时间复杂度均为 (O(m + n)),其中m
和n
分别是链表l1
和l2
的长度。这是因为在最坏情况下,需要比较两个链表中的每一个节点,直到其中一个链表遍历完。
空间复杂度
- 迭代法:除了返回的合并链表外,使用的额外空间为常数级,即用于创建哑节点和一些指针变量,空间复杂度为 (O(1))。
- 递归法:递归调用会使用额外的栈空间,递归的深度最大为
m + n
,因此空间复杂度为 (O(m + n)),这是由于在递归过程中,函数调用栈会保存每一层递归的状态,直到递归结束。
五、实现的关键点和难度
关键点
- 节点比较与拼接:无论是迭代法还是递归法,准确比较
l1
和l2
当前节点的值,并将较小值的节点正确拼接到合并链表中是核心操作。在迭代法中,通过cur
指针来跟踪并完成拼接;在递归法中,通过递归调用返回的节点来完成拼接。 - 边界条件处理:处理好链表为空的情况是关键。在迭代法中,开始时要判断
l1
和l2
是否为空,直接返回非空链表;在递归法中,递归的终止条件就是处理链表为空的情况,正确返回相应结果。另外,当一个链表遍历完后,要将另一个链表剩余部分直接接到合并链表的末尾。 - 链表指针操作:对于链表的指针操作要准确无误。在迭代法中,移动
cur
指针以及l1
和l2
的指针时要确保逻辑正确;在递归法中,设置节点的next
指针时要保证指向正确的节点。
难度
- 递归理解与实现:递归法虽然代码简洁,但对于不熟悉递归思想的人来说,理解递归的调用过程和状态保存可能有一定难度。特别是在处理递归终止条件和递归调用返回结果的拼接时,需要清晰的逻辑思维,否则容易出现错误。
- 指针操作细节:链表的指针操作容易出错,尤其是在同时处理多个链表指针时。在迭代法中,可能会出现指针移动顺序错误或遗漏指针更新的情况;在递归法中,对递归调用返回节点的指针设置也需要谨慎处理,否则可能导致链表结构混乱。
六、扩展及难度加深题目
扩展题目1:合并K个有序链表
- 题目描述:给定一个包含
K
个有序链表的数组,要求将这些链表合并成一个有序链表。 - 解题思路:可以使用分治法来解决。将
K
个链表两两分组,合并每组的两个链表,然后对合并后的链表继续分组合并,直到最终合并成一个链表。也可以依次将每个链表合并到一个结果链表中,但这种方法时间复杂度较高。 - 代码示例(以Python为例,使用分治法)
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def mergeKLists(self, lists):
if not lists:
return None
if len(lists) == 1:
return lists[0]
mid = len(lists) // 2
l1 = self.mergeKLists(lists[:mid])
l2 = self.mergeKLists(lists[mid:])
return self.mergeTwoLists(l1, l2)
def mergeTwoLists(self, l1, l2):
head = ListNode(0)
move = head
if not l1: return l2
if not l2: return l1
while l1 and l2:
if l1.val < l2.val:
move.next = l1
l1 = l1.next
else:
move.next = l2
l2 = l2.next
move = move.next
move.next = l1 if l1 else l2
return head.next
扩展题目2:合并两个有序链表并去重
- 题目描述:给定两个有序链表,合并它们并去除合并后链表中的重复节点。
- 解题思路:在合并链表的过程中,增加去重逻辑。当将节点拼接到合并链表时,检查当前节点值是否与前一个节点值相同,如果相同则跳过该节点。
- 代码示例(以Python为例)
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def mergeTwoListsAndRemoveDuplicates(self, l1, l2):
head = ListNode(0)
move = head
if not l1: return l2
if not l2: return l1
while l1 and l2:
if l1.val < l2.val:
if not move.next or move.next.val != l1.val:
move.next = l1
move = move.next
l1 = l1.next
else:
if not move.next or move.next.val != l2.val:
move.next = l2
move = move.next
l2 = l2.next
while l1:
if not move.next or move.next.val != l1.val:
move.next = l1
move = move.next
l1 = l1.next
while l2:
if not move.next or move.next.val != l2.val:
move.next = l2
move = move.next
l2 = l2.next
return head.next
难度加深题目1:合并两个有序链表并按特定规则排序
- 题目描述:给定两个有序链表,要求合并后按照一种特定规则排序,例如先按节点值的奇偶性排序(奇数在前,偶数在后),对于奇偶性相同的节点,再按值从小到大排序。
- 解题思路:在合并链表时,根据特定规则对节点进行比较和拼接。可以先分别遍历两个链表,将奇数节点和偶数节点分别存储到两个临时链表中,然后按照规则合并这两个临时链表。
- 代码示例(以Python为例)
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def mergeTwoListsWithCustomSort(self, l1, l2):
odd_head = ListNode(0)
odd_move = odd_head
even_head = ListNode(0)
even_move = even_head
while l1:
if l1.val % 2 == 1:
odd_move.next = l1
odd_move = odd_move.next
else:
even_move.next = l1
even_move = even_move.next
l1 = l1.next
while l2:
if l2.val % 2 == 1:
odd_move.next = l2
odd_move = odd_move.next
else:
even_move.next = l2
even_move = even_move.next
l2 = l2.next
odd_move.next = None
even_move.next = None
odd_head = self.sortList(odd_head.next)
even_head = self.sortList(even_head.next)
odd_move = odd_head
if odd_move:
while odd_move.next:
odd_move = odd_move.next
odd_move.next = even_head
return odd_head
return even_head
def sortList(self, head):
if not head or not head.next:
return head
slow, fast = head, head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
mid = slow.next
slow.next = None
left = self.sortList(head)
right = self.sortList(mid)
return self.mergeTwoLists(left, right)
def mergeTwoLists(self, l1, l2):
head = ListNode(0)
move = head
while l1 and l2:
if l1.val < l2.val:
move.next = l1
l1 = l1.next
else:
move.next = l2
l2 = l2.next
move = move.next
move.next = l1 if l1 else l2
return head.next
难度加深题目2:合并两个有序循环链表
- 题目描述:给定两个有序的循环链表,将它们合并成一个有序的循环链表。
- 解题思路:首先找到两个循环链表的尾节点,以便后续进行链表的拼接。然后,按照合并普通有序链表的方式合并两个链表,但在最后要注意将合并后的链表尾节点指向头节点,形成循环链表。在合并过程中,需要特别处理边界情况,比如其中一个链表为空,或者两个链表合并后只有一个节点的情况。
- 代码示例(以Python为例)
# Definition for a circular singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def mergeTwoCircularLists(self, l1, l2):
if not l1:
return l2
if not l2:
return l1
# 找到l1的尾节点
tail1 = l1
while tail1.next!= l1:
tail1 = tail1.next
# 找到l2的尾节点
tail2 = l2
while tail2.next!= l2:
tail2 = tail2.next
new_head = ListNode(0)
move = new_head
cur1, cur2 = l1, l2
while cur1!= l1 or cur2!= l2:
if cur2 == l2 or (cur1!= l1 and cur1.val < cur2.val):
move.next = cur1
move = move.next
cur1 = cur1.next
if cur1 == l1:
move.next = cur2
move = move.next
cur2 = cur2.next
else:
move.next = cur2
move = move.next
cur2 = cur2.next
if cur2 == l2:
move.next = cur1
move = move.next
cur1 = cur1.next
# 形成循环链表
move.next = new_head.next
return new_head.next
七、应用场合
- 数据整合与排序:在数据库操作中,当从不同的有序数据源获取数据(例如多个有序的查询结果集),需要将这些数据合并成一个有序集合时,可以使用合并有序链表的方法。例如,在一个电商系统中,不同类别的商品库存信息可能存储在不同的有序链表中,通过合并这些链表可以得到一个完整的有序库存列表,方便进行库存管理和查询。
- 多路归并排序:合并K个有序链表的方法是多路归并排序的基础。在处理大规模数据时,将数据分成多个有序的子部分,然后通过合并这些有序子部分来实现整体排序。这种方法常用于外部排序算法中,当数据量太大无法一次性加载到内存时,可利用外存(如磁盘)存储有序子文件,再通过合并操作得到最终的有序数据。
- 图算法中的最短路径搜索:在一些图算法中,如Dijkstra算法求最短路径,可能会涉及到合并有序链表的操作。在搜索过程中,不同路径的距离信息可能以有序链表的形式存储,通过合并这些链表可以更新和维护最短路径的信息。
- 操作系统中的任务调度:在操作系统的任务调度模块中,如果有多个优先级队列,每个队列中的任务按照某种规则(如执行时间、优先级等)有序排列。当需要综合考虑所有任务进行调度时,就需要合并这些有序队列,类似于合并有序链表的操作,以确定下一个执行的任务。
进一步思考与优化方向
- 算法优化
- 减少比较次数:在合并两个有序链表时,虽然目前的方法已经是线性时间复杂度 (O(m + n)),但在某些特殊情况下,可以通过更智能的比较策略减少比较次数。例如,如果知道两个链表长度的大致范围,可以先比较较长链表头部和较短链表尾部的值,提前确定一些节点的合并顺序,从而减少不必要的比较。
- 并行处理:对于合并K个有序链表的问题,在多核处理器环境下,可以考虑并行化合并过程。将K个链表分成若干组,并行地合并每组内的链表,然后再依次合并这些中间结果,这样可以利用多核优势,在理论上提高合并效率。
- 内存管理优化
- 避免不必要的内存分配:在迭代实现合并链表时,尤其是在C++ 中,要注意内存的分配和释放。例如,创建哑节点时,如果在函数结束时没有正确释放相关内存,可能会导致内存泄漏。可以考虑使用智能指针(如
std::unique_ptr
或std::shared_ptr
)来管理链表节点的内存,确保内存能够被正确释放。 - 优化数据结构使用:在某些场景下,如果链表中的数据量非常大,并且频繁进行合并操作,可以考虑使用更适合的底层数据结构来实现链表,例如跳表(Skip List)。跳表在保持链表特性的同时,通过增加额外的索引层,可以在 (O(\log n)) 的时间复杂度内完成查找、插入和删除操作,对于频繁合并和查找操作可能会提高整体性能。
- 避免不必要的内存分配:在迭代实现合并链表时,尤其是在C++ 中,要注意内存的分配和释放。例如,创建哑节点时,如果在函数结束时没有正确释放相关内存,可能会导致内存泄漏。可以考虑使用智能指针(如
- 边界情况与异常处理
- 空指针检查的完整性:在所有语言的实现中,都需要确保对空指针的检查是完整的。除了在函数开始时检查输入链表是否为空,在迭代或递归过程中,也要防止出现空指针引用的情况。例如,在比较节点值和移动指针时,要确保指针指向的节点存在,以避免程序崩溃。
- 处理特殊输入:除了处理空链表的情况,还应考虑其他特殊输入。比如两个链表都只有一个节点,或者一个链表为空而另一个链表很长的情况。对这些特殊情况进行充分测试,可以提高代码的鲁棒性。
- 代码风格与可读性
- 注释与命名规范:良好的注释和命名规范可以提高代码的可读性。在代码中,对关键步骤和复杂逻辑添加注释,使其他开发者能够快速理解代码的功能和实现思路。同时,使用有意义的变量名,例如用
cur
表示当前节点,dummy
表示哑节点等,使代码更加清晰易懂。 - 模块化设计:对于复杂的扩展题目,如合并K个有序链表或按特定规则合并链表,可以将相关功能封装成独立的函数或模块。这样不仅可以提高代码的复用性,还能使整体代码结构更加清晰,便于维护和调试。
- 注释与命名规范:良好的注释和命名规范可以提高代码的可读性。在代码中,对关键步骤和复杂逻辑添加注释,使其他开发者能够快速理解代码的功能和实现思路。同时,使用有意义的变量名,例如用
总结与启示
- 链表操作的核心要点:合并有序链表问题涵盖了链表操作的多个核心要点,包括节点的比较、指针的移动和链表的拼接。通过解决这类问题,我们深入理解了链表数据结构的特性以及如何灵活运用指针来实现复杂的操作。无论是迭代还是递归的方法,都需要精确控制指针的指向和移动,确保链表结构的完整性和正确性。
- 算法设计的灵活性:本题的多种解法以及扩展题目展示了算法设计的灵活性。迭代法直观且易于理解,递归法简洁明了,能够体现问题的递归本质。在面对不同的问题场景时,我们需要根据具体需求选择合适的解法。同时,通过对题目进行扩展和难度加深,我们学会了如何将基本算法思想应用于更复杂的问题,培养了从简单问题到复杂问题的迁移能力。
- 实际应用的映射:合并有序链表的算法在实际应用中有着广泛的场景,从数据处理到算法优化,再到操作系统的任务调度等领域都有体现。这启示我们,算法学习不仅仅是为了解决理论问题,更重要的是能够将算法思想应用到实际项目中,解决实际业务需求。通过对这些应用场景的理解,我们可以更好地体会算法的价值和意义,从而激发学习算法的兴趣和动力。
- 持续学习与提升:即使是看似简单的合并有序链表问题,也存在诸多可以深入思考和优化的方向。这提醒我们在学习算法的过程中,不能仅仅满足于实现基本功能,还需要不断探索算法的优化、边界情况处理以及代码的可读性和可维护性。持续学习和深入思考能够帮助我们提升算法能力,更好地应对复杂多变的实际问题。
通过对“合并两个有序链表”问题的全面分析和拓展,我们不仅掌握了具体的算法实现,还在算法优化、内存管理、代码规范等方面得到了提升,为解决更复杂的算法问题和实际应用奠定了坚实的基础。