LeetCode Hot 100——21.合并两个有序列表

21. 合并两个有序链表

题目

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

示例 1:

img

输入: l1 = [1,2,4], l2 = [1,3,4]
输出: [1,1,2,3,4,4]

示例 2:

输入: l1 = [], l2 = []
输出: []

示例 3:

输入: l1 = [], l2 = [0]
输出: [0]


一、迭代法

思路

合并两个有序链表的过程,类似于合并两个有序数组:每次都从两个链表的当前节点中取出值较小的那个,接到新链表的末尾。

但是单链表操作中,需要处理一个边界问题——新链表的头节点从哪来?

常见做法是创建一个哑节点(dummy node)。哑节点是一个临时的头节点,不存储实际数据,其 next 最终指向合并后链表的真正头节点。这样做的好处是:

  • 避免单独判断新链表的第一个节点是来自 list1 还是 list2
  • prev 指针始终指向新链表的尾节点,统一了插入逻辑。

代码实现

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    struct ListNode dummy;          // 栈上分配的哑节点
    struct ListNode* prev = &dummy; // prev 指向新链表的尾节点(初始为哑节点)
    dummy.next = NULL;

    while (list1 && list2) {
        if (list1->val <= list2->val) {
            prev->next = list1;     // 将 list1 接到新链表尾部
            list1 = list1->next;
        } else {
            prev->next = list2;     // 将 list2 接到新链表尾部
            list2 = list2->next;
        }
        prev = prev->next;          // prev 后移,始终指向新链表当前的尾节点
    }

    // 将剩余未遍历完的链表直接接上
    prev->next = list1 ? list1 : list2;

    return dummy.next;              // 哑节点的 next 即为合并后的头节点
}

代码解释

  1. struct ListNode dummy;:在栈上创建哑节点。之所以能用栈分配,是因为 dummy 的生命周期仅在此函数内,我们最终返回的是 dummy.next(指向堆上已有节点的指针),不会返回悬垂指针。

  2. struct ListNode* prev = &dummy;prev 称为前驱(或尾)指针,始终指向新链表的最后一个节点。初始时新链表为空,所以它指向哑节点。注意,这里 prev 保存的是 dummy地址,通过它可以修改 dummy.next(第一次插入时)或后续节点的 next

  3. while (list1 && list2):只要两个链表都还有节点,就进行比较。取 val 较小的节点接到 prev->next,然后对应链表的指针后移。完成后,prev = prev->nextprev 移到新的尾节点。

  4. prev->next = list1 ? list1 : list2;:循环结束后,必然有一个链表已为空,另一个可能还有剩余节点。因为原链表本身就是有序的,所以直接将剩余部分接到新链表尾部即可。这是链表合并中很优雅的一步:不需要像数组那样逐个复制剩余元素。

  5. return dummy.next;:哑节点的 next 指向合并后链表的第一个有效节点。

核心技巧:为什么用哑节点?

如果不使用哑节点,需要额外判断 prev 是否为 NULL(即是否是第一次插入):

// 不使用哑节点的写法(需单独处理头节点)
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    struct ListNode* head = NULL;
    struct ListNode** prev = &head;  // 二级指针,指向 head

    while (list1 && list2) {
        if (list1->val <= list2->val) {
            *prev = list1;
            list1 = list1->next;
        } else {
            *prev = list2;
            list2 = list2->next;
        }
        prev = &((*prev)->next);
    }
    *prev = list1 ? list1 : list2;
    return head;
}

可以看到,不用哑节点时需要借助二级指针 struct ListNode** 来实现同样的效果。哑节点方案在逻辑上更直观,是链表操作中的常用技巧。

复杂度分析

  • 时间复杂度: $O(m + n)$,其中 mn 分别是两个链表的长度。每次循环都会将其中一个链表的指针后移,直到所有节点都被处理。
  • 空间复杂度: $O(1)$,只使用了常数额外空间(哑节点在栈上分配,不随输入规模变化)。

二、递归法

思路

递归法的核心思想是:比较两个链表当前头节点的值,取较小的节点,然后将其 next 指向剩余节点合并后的结果

递归的终止条件是:其中一个链表为空,直接返回另一个链表。

代码实现

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    // 递归终止条件:若其中一个链表为空,直接返回另一个
    if (list1 == NULL) return list2;
    if (list2 == NULL) return list1;

    // 取较小的节点,递归合并剩余部分
    if (list1->val <= list2->val) {
        list1->next = mergeTwoLists(list1->next, list2);
        return list1;
    } else {
        list2->next = mergeTwoLists(list1, list2->next);
        return list2;
    }
}

递归过程模拟

list1 = [1, 2, 4]list2 = [1, 3, 4] 为例:

merge(1→2→4, 1→3→4)
  ├─ 1≤1 → list1->next = merge(2→4, 1→3→4)
  │    ├─ 2>1 → list2->next = merge(2→4, 3→4)
  │    │    ├─ 2≤3 → list1->next = merge(4, 3→4)
  │    │    │    ├─ 4>3 → list2->next = merge(4, 4)
  │    │    │    │    ├─ 4≤4 → list1->next = merge(NULL, 4)
  │    │    │    │    │    └─ 返回 4
  │    │    │    │    └─ 返回 4→4
  │    │    │    └─ 返回 3→4→4
  │    │    └─ 返回 2→3→4→4
  │    └─ 返回 1→2→3→4→4
  └─ 返回 1→1→2→3→4→4

复杂度分析

  • 时间复杂度: $O(m + n)$,每个节点恰好被递归处理一次。
  • 空间复杂度: $O(m + n)$,递归调用栈的深度等于合并后链表的长度。当链表很长时,递归可能导致栈溢出——这也是迭代法更实用的原因。
posted @ 2026-07-20 11:22  Javenwww  阅读(2)  评论(0)    收藏  举报