概念
单链表的基本技巧,每个技巧都对应着至少一道算法题:
1、合并两个有序链表
2、链表的分解
3、合并 k 个有序链表
4、寻找单链表的倒数第 k 个节点
5、寻找单链表的中点
6、判断单链表是否包含环并找出环起点
7、判断两个单链表是否相交并找出交点
题目
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入: l1 = [1,2,4], l2 = [1,3,4]
输出: [1,1,2,3,4,4]
示例 2:
输入: l1 = [], l2 = []
输出: []
示例 3:
输入: l1 = [], l2 = [0]
输出: [0]
提示:
- 两个链表的节点数目范围是
[0, 50] -100 <= Node.val <= 100l1和l2均按 非递减顺序 排列
解题思路🙋🏻 ♀️
注意事项
- 虚拟头节点的使用:为了简化代码和避免处理合并链表的头节点为特殊情况,我们使用了一个虚拟的头节点
dammy。这使得代码更简洁,但使用后必须记住返回dammy.next而不是dammy作为结果链表的头。 - 强制解包的风险:在代码中使用
p = p.next!进行了强制解包。虽然在当前的逻辑下是安全的,但强制解包总是有风险的。在其他上下文或逻辑变化后,这可能会导致运行时崩溃。 - 指针的移动:当决定从
l1或l2中取一个节点加入结果链表时,除了更新p.next,还需要移动相应的l1Current或l2Current指针。 - 循环终止条件:主循环的条件是
l1Current和l2Current都不为nil。当一个链表耗尽时,循环结束,然后可以直接连接另一个未耗尽的链表的剩余部分。 - 代码的可读性:使用有意义的变量名和适当的注释可以大大增加代码的可读性。例如,
l1Current和l2Current比简单的a和b更具描述性。
思考🤔
Q:为什么要用 p, 而不是直接使用 dammy ? let dammy = ListNode(0) var p = dammy ?
A: 使用 dammy 和 p 两个变量的目的是为了区分链表的起点和当前操作的节点。
- dammy 是一个虚拟的头节点,它的主要作用是为了简化边界条件的处理。在整个过程中,我们不会移动
dammy。这样,在操作结束后,我们可以直接通过dammy.next来获取合并后的链表的头部。 - p 是一个工作指针,它表示我们当前正在操作的位置。当我们决定将
l1Current或l2Current连接到结果链表时,我们会使用p.next来完成连接,并将p移动到p.next,表示已经处理了当前节点。
如果只使用 dammy 并尝试同时完成两个任务,代码会变得复杂并且更容易出错。例如,每次连接新节点时,你需要遍历整个链表以找到末尾的节点,这会使算法的时间复杂度从 O(n) 增加到 O(n2)。
p = p.next! 这个代码不会出错吗?
- 在每次循环中,我们都根据
l1Current和l2Current的值设置了p.next。 - 在循环结束后,我们也确保了至少有一个列表不为空,并将其余部分连接到
p.next。
代码
import Foundation
class Solution {
func mergeTwoLists(_ list1: ListNode?, _ list2: ListNode?) -> ListNode? {
// 创建两个指针分别指向两个链表的头部
var l1Current = list1
var l2Current = list2
// 创建一个哑结点(dummy node)来帮助合并
var dammy = ListNode(0)
// p 指针始终指向最后一个被合并的结点
var p = dammy
// 当两个链表都还有结点时,继续合并
while l1Current != nil && l2Current != nil {
// 比较两个链表当前结点的值,选择较小的那个结点进行合并
if l1Current!.val < l2Current!.val {
p.next = l1Current
l1Current = l1Current?.next // 移动 l1Current 指针到下一个结点
} else {
p.next = l2Current
l2Current = l2Current?.next // 移动 l2Current 指针到下一个结点
}
p = p.next! // 移动 p 指针到最后一个被合并的结点
}
// 如果第一个链表还有剩余结点,直接将它们添加到结果链表的末尾
if l1Current != nil {
p.next = l1Current
}
// 如果第二个链表还有剩余结点,直接将它们添加到结果链表的末尾
if l2Current != nil {
p.next = l2Current
}
// 返回合并后的链表,从哑结点的下一个结点开始
return dammy.next
}
}
时空复杂度分析
时空复杂度 O(n)
引用
本系列文章部分概念内容引用 www.hello-algo.com/
解题思路参考了 abuladong 的算法小抄, 代码随想录... 等等
Youtube 博主: huahua 酱, 山景城一姐,