Swift 数据结构与算法( 17 ) 链表 + S_Leetcode21. 合并两个有序链表

98 阅读1分钟

概念

单链表的基本技巧,每个技巧都对应着至少一道算法题:

1、合并两个有序链表

2、链表的分解

3、合并 k 个有序链表

4、寻找单链表的倒数第 k 个节点

5、寻找单链表的中点

6、判断单链表是否包含环并找出环起点

7、判断两个单链表是否相交并找出交点

题目

21. 合并两个有序链表

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

 

示例 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 <= 100
  • l1 和 l2 均按 非递减顺序 排列

解题思路🙋🏻‍ ♀️

注意事项

  1. 虚拟头节点的使用:为了简化代码和避免处理合并链表的头节点为特殊情况,我们使用了一个虚拟的头节点 dammy。这使得代码更简洁,但使用后必须记住返回 dammy.next 而不是 dammy 作为结果链表的头。
  2. 强制解包的风险:在代码中使用 p = p.next! 进行了强制解包。虽然在当前的逻辑下是安全的,但强制解包总是有风险的。在其他上下文或逻辑变化后,这可能会导致运行时崩溃。
  3. 指针的移动:当决定从 l1l2 中取一个节点加入结果链表时,除了更新 p.next,还需要移动相应的 l1Currentl2Current 指针。
  4. 循环终止条件:主循环的条件是 l1Currentl2Current 都不为 nil。当一个链表耗尽时,循环结束,然后可以直接连接另一个未耗尽的链表的剩余部分。
  5. 代码的可读性:使用有意义的变量名和适当的注释可以大大增加代码的可读性。例如,l1Currentl2Current 比简单的 ab 更具描述性。

思考🤔

Q:为什么要用 p, 而不是直接使用 dammy ? let dammy = ListNode(0) var p = dammy ?

A: 使用 dammyp 两个变量的目的是为了区分链表的起点和当前操作的节点。

  1. dammy 是一个虚拟的头节点,它的主要作用是为了简化边界条件的处理。在整个过程中,我们不会移动 dammy。这样,在操作结束后,我们可以直接通过 dammy.next 来获取合并后的链表的头部。
  2. p 是一个工作指针,它表示我们当前正在操作的位置。当我们决定将 l1Currentl2Current 连接到结果链表时,我们会使用 p.next 来完成连接,并将 p 移动到 p.next,表示已经处理了当前节点。

如果只使用 dammy 并尝试同时完成两个任务,代码会变得复杂并且更容易出错。例如,每次连接新节点时,你需要遍历整个链表以找到末尾的节点,这会使算法的时间复杂度从 O(n) 增加到 O(n2)

p = p.next! 这个代码不会出错吗?

  1. 在每次循环中,我们都根据 l1Currentl2Current 的值设置了 p.next
  2. 在循环结束后,我们也确保了至少有一个列表不为空,并将其余部分连接到 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)

截屏2023-08-05 17.03.34.png

引用

本系列文章部分概念内容引用 www.hello-algo.com/

解题思路参考了 abuladong 的算法小抄, 代码随想录... 等等

Youtube 博主: huahua 酱, 山景城一姐,