跳转至

AC208-将两个递增有序的单链表 B 合并到 A 中,形成一个非递增有序的链表

代码实现

C
// 函数 merge:将两个递增有序的单链表 B 合并到 A 中,形成一个非递增(递减)有序的链表
void merge(LinkList A, LinkList B) {
    // p 指向链表 A 的第一个数据节点,q 指向链表 B 的第一个数据节点
    LinkList p = A->next, q = B->next, r;

    // 将 A 和 B 的头节点的 next 指针置空,准备重新构建链表
    A->next = NULL;
    B->next = NULL;

    // 同时遍历两个链表,比较节点数据大小
    while (p != NULL && q != NULL) {
        // 选择较小的节点插入到结果链表的前端(头插法)
        if (p->data <= q->data) {
            r = p;        // 选择 p 节点
            p = p->next;  // 移动 p 指针到下一个节点
        } else {
            r = q;        // 选择 q 节点
            q = q->next;  // 移动 q 指针到下一个节点
        }
        // 头插法:将选中的节点插入到结果链表的前端
        r->next = A->next;
        A->next = r;
    }

    // 处理链表 A 中剩余的节点
    while (p != NULL) {
        r = p;           // 选择剩余的 p 节点
        p = p->next;     // 移动 p 指针
        r->next = A->next; // 头插法插入
        A->next = r;
    }

    // 处理链表 B 中剩余的节点
    while (q != NULL) {
        r = q;           // 选择剩余的 q 节点
        q = q->next;     // 移动 q 指针
        r->next = A->next; // 头插法插入
        A->next = r;
    }
}

代码逻辑解析

  1. 初始化指针
  2. p 指向链表 A 的第一个数据节点(跳过头节点)
  3. q 指向链表 B 的第一个数据节点(跳过头节点)
  4. r 用于临时存储当前要处理的节点

  5. 清空头节点

  6. 将 A 和 B 的头节点的 next 指针置空,为重新构建链表做准备

  7. 合并过程

  8. 使用头插法将节点逐个插入到链表 A 中
  9. 每次比较 pq 指向节点的数据,选择较小的节点插入
  10. 由于是头插法,较小的元素会插入到链表前端,最终形成递减序列

  11. 处理剩余节点

  12. 当其中一个链表遍历完后,将另一个链表的剩余节点全部插入

时间复杂度分析

  • 节点遍历
  • 需要遍历链表 A 的所有节点(最多 m 个)
  • 需要遍历链表 B 的所有节点(最多 n 个)
  • 每个节点只被访问一次

  • 操作复杂度

  • 每次操作(比较、指针移动、插入)都是常数时间

因此,时间复杂度为 O(m + n),其中 m 和 n 分别是链表 A 和 B 的长度。


空间复杂度分析

  • 额外空间使用
  • 只使用了常量级别的额外指针变量(p, q, r)
  • 没有使用与输入规模相关的额外存储空间

因此,空间复杂度为 O(1)


较难理解部分的说明

为什么使用头插法能形成非递增序列?

关键在于贪心策略:每次都选择当前两个链表中较小的元素插入到结果链表的前端。

图解说明

假设: - 链表 A:1 -> 4 -> 7 - 链表 B:2 -> 5 -> 8

合并过程:

Text Only
初始状态:
A: 头节点 -> NULL
B: 头节点 -> NULL
p: 1 -> 4 -> 7
q: 2 -> 5 -> 8

步骤1: 比较 1 和 2,选择 1
A: 头节点 -> 1 -> NULL
p: 4 -> 7
q: 2 -> 5 -> 8

步骤2: 比较 4 和 2,选择 2
A: 头节点 -> 2 -> 1 -> NULL
p: 4 -> 7
q: 5 -> 8

步骤3: 比较 4 和 5,选择 4
A: 头节点 -> 4 -> 2 -> 1 -> NULL
p: 7
q: 5 -> 8

步骤4: 比较 7 和 5,选择 5
A: 头节点 -> 5 -> 4 -> 2 -> 1 -> NULL
p: 7
q: 8

步骤5: 比较 7 和 8,选择 7
A: 头节点 -> 7 -> 5 -> 4 -> 2 -> 1 -> NULL
p: NULL
q: 8

步骤6: 处理剩余节点 8
A: 头节点 -> 8 -> 7 -> 5 -> 4 -> 2 -> 1 -> NULL

最终结果:8 -> 7 -> 5 -> 4 -> 2 -> 1(非递增序列)


延伸知识点

1. 头插法与尾插法的区别

头插法(本题使用):

C
r->next = A->next;
A->next = r;
- 优点:实现简单,不需要额外的尾指针 - 结果:逆序排列

尾插法

C
1
2
3
tail->next = r;
tail = r;
tail->next = NULL;
- 优点:保持原有顺序 - 缺点:需要维护尾指针

2. 相关变体问题

变体1:合并为递增序列

C
// 如果要求递增序列,可以先用头插法得到递减序列,再反转链表
// 或者使用尾插法直接构建递增序列

变体2:原地合并(不使用额外节点)

C
void mergeAscending(LinkList A, LinkList B) {
    LinkList pa = A, pb = B->next; // pa 是前驱指针
    while (pb != NULL) {
        LinkList temp = pb;
        pb = pb->next;
        // 找到合适的插入位置
        while (pa->next != NULL && pa->next->data < temp->data) {
            pa = pa->next;
        }
        temp->next = pa->next;
        pa->next = temp;
        pa = A; // 重置前驱指针
    }
}

3. 算法优化

优化版本(减少重复代码):

C
void mergeOptimized(LinkList A, LinkList B) {
    LinkList p = A->next, q = B->next, r;
    A->next = NULL;
    B->next = NULL;

    // 主合并循环
    while (p != NULL || q != NULL) {
        if (q == NULL || (p != NULL && p->data <= q->data)) {
            r = p;
            p = p->next;
        } else {
            r = q;
            q = q->next;
        }
        r->next = A->next;
        A->next = r;
    }
}

4. 实际应用场景

  • 归并排序:链表归并排序中的合并步骤
  • 数据库合并:合并两个已排序的数据集
  • 文件处理:合并两个已排序的文件记录

这种方法体现了分治思想在链表操作中的应用,通过简单的比较和插入操作实现了高效的有序合并。