LeetCode 21. 合并两个有序链表(易)& 剑指offer 面试题25. 合并两个排序的链表(易)

链表结合迭代思想解决该问题

题目

将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

测试用例

功能测试(输入的两个链表有多个节点;节点的值互不相同或者存在值相等的多个节点)。 特殊测试(两个链表的一个或者两个头节点为 nullptr 指针;两个链表中只有一个节点)。

本题考点

考查应聘者分析问题的能力。解决这个问题需要大量的指针操作,应聘者如果没有透彻地分析问题形成清晰的思路,则很难写出正确的代码。 考查应聘者能不能写出鲁棒的代码。由于有大量指针操作,应聘者如果稍有不慎就会在代码中遗留很多与鲁棒性相关的隐患。建议应聘者在写代码之前全面分析哪些情况会引入空指针,并考虑清楚怎么处理这些空指针。

示例

示例1

1
2
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

考察知识点

链表、递归、迭代

核心思想

Python Trick

将传入的链表转化成list,相加排序之后再转换回去,注意做特判。

递归

我们可以如下递归地定义在两个链表里的 merge 操作(忽略边界情况,比如空链表等):

\(\left\{ \begin{array}{ll} list1[0] + merge(list1[1:], list2) & list1[0] < list2[0] \\ list2[0] + merge(list1, list2[1:]) & otherwise \end{array} \right.\)

也就是说,两个链表头部较小的一个与剩下元素的 merge 操作结果合并。
我们直接将以上递归过程建模,首先考虑边界情况。
1、特殊的,如果 \(l1\) 或者 \(l2\) 一开始就是 \(None\) ,那么没有任何操作需要合并,所以我们只需要返回非空链表。
2、否则,我们要判断 \(l1\)\(l2\) 哪一个的头元素更小,然后递归地决定下一个添加到结果里的值。
3、如果两个链表都是空的,那么过程终止,所以递归过程最终一定会终止。

迭代

我们可以用迭代的方法来实现。我们假设 \(l1\) 元素严格比 \(l2\)元素少,我们可以将 \(l2\) 中的元素逐一插入 \(l1\) 中正确的位置。

算法
1、首先,我们设定一个哨兵节点 "prehead" ,这可以在最后让我们比较容易地返回合并后的链表。
2、我们维护一个 prev 指针,我们需要做的是调整它的 next 指针。然后,我们重复以下过程,直到 l1 或者 l2 指向了 None
2.1、如果 l1 当前位置的值小于等于 l2 ,我们就把 l1 的值接在 prev 节点的后面同时将 l1 指针往后移一个。
2.2、 否则,我们对 l2 做同样的操作。
2.3、不管我们将哪一个元素接在了后面,我们都把 prev 向后移一个元素。
3、在循环终止的时候, l1l2 至多有一个是非空的。由于输入的两个链表都是有序的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。这意味着我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表。

ef59e3a1e840cbe64bf988b85d4967e120605ec0fcb5686c8e0910306f12999b-image.pngLeetCode题解

Python版本

方法一的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
if l1 is None:
return l2
if l2 is None:
return l1
if listLength(l1):
l1 = convert2list(l1)
else:
l1 = []
if listLength(l2):
l2 = convert2list(l2)
else:
l2 = []
l = l1 + l2

if len(l):
l.sort()
return createList(l)

时间复杂度 \(O(n)\)
执行用时 :48 ms, 在所有 Python3 提交中击败了29.79%的用户
内存消耗 :13.6 MB, 在所有 Python3 提交中击败了40.52%的用户

递归的实现

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def mergeTwoLists(self, l1, l2):
if l1 is None:
return l2
elif l2 is None:
return l1
elif l1.val < l2.val:
l1.next = self.mergeTwoLists(l1.next, l2) # 保留那个最小的,其余扔进去继续找最小的
return l1 # 将处理的l1返回回去,因为l1.val比l2.val要小一些。
else:
l2.next = self.mergeTwoLists(l1, l2.next) # 同上
return l2 # 将处理的l2返回回去,因为l2.val比l1.val要小一些。

时间复杂度:\(O(n + m)\)。 因为每次调用递归都会去掉 \(l1\) 或者 \(l2\) 的头元素(直到至少有一个链表为空),函数 mergeTwoList 中只会遍历每个元素一次。所以,时间复杂度与合并后的链表长度为线性关系。 空间复杂度:\(O(n + m)\)。调用 mergeTwoLists 退出时 \(l1\)\(l2\) 中每个元素都一定已经被遍历过了,所以 \(n + m\)个栈帧会消耗 \(O(n + m)\)的空间。
执行用时 :36 ms, 在所有 Python3 提交中击败了85.48%的用户
内存消耗 :13.3 MB, 在所有 Python3 提交中击败了41.12%的用户

迭代的实现 推荐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def mergeTwoLists(self, l1, l2):
prehead = ListNode(-1) # 设定一个哨兵节点 "prehead" ,这可以在最后让我们比较容易地返回合并后的链表。
prev = prehead # prev会一直跟在那个小的节点的链表后面,最后串起来一个新的有序链表
while l1 and l2:
if l1.val <= l2.val: # 如果 l1 当前位置的值小于等于 l2 ,我们就把 l1 的值接在 prev 节点的后面同时将 l1 指针往后移一个。
prev.next = l1
l1 = l1.next
else: # 否则,我们对 l2 做同样的操作。不管我们将哪一个元素接在了后面,我们都把 prev 向后移一个元素。
prev.next = l2
l2 = l2.next
prev = prev.next # 不管我们将哪一个元素接在了后面,我们都把 prev 向后移一个元素。
prev.next = l1 if l1 is not None else l2 # 在循环终止的时候, l1 和 l2 至多有一个是非空的。由于输入的两个链表都是有序的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。这意味着我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表。

return prehead.next

时间复杂度:\(O(n + m)\)。因为每次循环迭代中,l1l2 只有一个元素会被放进合并链表中, while 循环的次数等于两个链表的总长度。所有其他工作都是常数级别的,所以总的时间复杂度是线性的。
空间复杂度:\(O(1)\) 。迭代的过程只会产生几个指针,所以它所需要的空间是常数级别的。
执行用时 :40 ms, 在所有 Python3 提交中击败了66.60%的用户
内存消耗 :13.5 MB, 在所有 Python3 提交中击败了41.03%的用户