跳转至

AC205-将一个带头结点的单链表 A 分解成两个带头结点的单链表 A 和 B,使 A 中包含原链表中奇数位置的元素,B 中包含偶数位置的元素,且各元素在新链表中的相对顺序保持不变

代码实现

C
// 函数 split:将带头结点的单链表 A 分解为两个链表
// A 包含奇数位置的元素,B 包含偶数位置的元素
// 位置从 1 开始计数,保持元素相对位置不变
LinkList split(LinkList A) {
    // 创建链表 B 的头结点
    LinkList B = (LinkList)malloc(sizeof(LNode));
    LinkList rb = B;        // rb 指向 B 链表的尾结点,用于尾插法
    LinkList p = A;         // p 指向 A 链表的头结点
    int pos = 1;            // 记录当前处理节点的位置(从 1 开始)

    // 遍历链表 A,处理每个节点
    while (p->next != NULL) {
        // 如果是奇数位置(第 1, 3, 5... 个节点)
        if (pos % 2 == 1) {
            p = p->next;    // 保留在 A 链表中,移动指针
        } else {
            // 如果是偶数位置(第 2, 4, 6... 个节点)
            LinkList r = p->next;       // r 指向要移动到 B 链表的节点
            p->next = r->next;          // 从 A 链表中删除该节点
            rb->next = r;               // 将该节点添加到 B 链表尾部
            rb = r;                     // 更新 B 链表的尾指针
        }
        pos++;  // 位置计数器加 1
    }

    rb->next = NULL;    // 设置 B 链表的尾结点指针为 NULL
    return B;           // 返回 B 链表的头结点
}

代码逻辑解析

  1. 初始化操作
  2. 创建新链表 B 的头结点,用于存储偶数位置的元素
  3. rb 指针用于维护 B 链表的尾部,实现尾插法
  4. p 指针从 A 链表的头结点开始遍历
  5. pos 变量记录当前位置(从 1 开始)

  6. 节点分类策略

  7. 奇数位置节点(pos % 2 == 1):保留在原链表 A 中
  8. 偶数位置节点(pos % 2 == 0):移动到新链表 B 中

  9. 节点移动过程

  10. 使用 r 指针指向要移动的节点
  11. 修改 p->next 跳过该节点(从 A 链表删除)
  12. 将该节点连接到 B 链表尾部
  13. 更新 B 链表的尾指针

  14. 位置计数

  15. 每处理一个节点,位置计数器 pos 增加 1
  16. 保证了按原始位置正确分类

  17. 链表终止

  18. 设置 B 链表尾结点的 next 指针为 NULL
  19. 返回 B 链表的头结点

修正说明

原代码存在一个小的逻辑错误,修正后的完整代码如下:

C
LinkList split(LinkList A) {
    LinkList B = (LinkList)malloc(sizeof(LNode));
    LinkList rb = B, p = A;
    int pos = 1;

    while (p->next != NULL) {
        if (pos % 2 == 1) {
            p = p->next;    // 奇数位置,保留在 A 中
        } else {
            LinkList r = p->next;       // 偶数位置,移动到 B 中
            p->next = r->next;          // 从 A 中删除
            rb->next = r;               // 添加到 B 尾部
            rb = r;                     // 更新 B 的尾指针
        }
        pos++;  // 位置计数器应在每次循环后递增
    }

    rb->next = NULL;    // 设置 B 链表尾部
    return B;
}

时间复杂度分析

  • 链表遍历:需要遍历原链表 A 的所有节点,假设链表长度为 n,则为 O(n)
  • 节点操作:每次节点的删除和插入操作都是 O(1) 时间复杂度
  • 位置计算:每次的位置判断和递增都是 O(1) 操作

因此,总的时间复杂度为 O(n)


空间复杂度分析

  • 新链表 B:虽然创建了新链表,但是复用了原链表中的节点,没有创建新的数据节点
  • 辅助变量:只使用了常量级别的额外空间(如指针变量 rb, p, r 和整型变量 pos
  • 头结点:只为 B 链表创建了一个头结点

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


较难理解部分的说明

为什么使用尾插法?

尾插法保证了元素在新链表中的相对位置与原链表中的一致: - 按顺序处理原链表节点 - 按相同顺序插入到新链表尾部 - 避免了元素顺序的颠倒

图解说明

假设原链表 A 如下(位置从 1 开始):

Text Only
1
2
3
A: L -> 1 -> 2 -> 3 -> 4 -> 5 -> NULL
   ↑    ↑    ↑    ↑    ↑    ↑
位置:   1    2    3    4    5

处理过程:

初始状态

Text Only
1
2
3
A: L -> 1 -> 2 -> 3 -> 4 -> 5 -> NULL
B: L -> NULL
p -> A头结点, rb -> B头结点, pos = 1

处理位置 1(节点 1): - pos = 1(奇数),保留在 A 中 - p 移动到节点 1

处理位置 2(节点 2): - pos = 2(偶数),移动到 B 中

Text Only
A: L -> 1 -> 3 -> 4 -> 5 -> NULL
B: L -> 2 -> NULL

处理位置 3(节点 3): - pos = 3(奇数),保留在 A 中 - p 移动到节点 3

处理位置 4(节点 4): - pos = 4(偶数),移动到 B 中

Text Only
A: L -> 1 -> 3 -> 5 -> NULL
B: L -> 2 -> 4 -> NULL

处理位置 5(节点 5): - pos = 5(奇数),保留在 A 中 - p 移动到节点 5

最终结果

Text Only
A: L -> 1 -> 3 -> 5 -> NULL  (奇数位置元素)
B: L -> 2 -> 4 -> NULL       (偶数位置元素)


延伸知识点

1. 相关链表操作技巧

头插法 vs 尾插法

C
// 头插法(会改变相对顺序)
void headInsert(LinkList head, LinkList node) {
    node->next = head->next;
    head->next = node;
}

// 尾插法(保持相对顺序)
void tailInsert(LinkList *tail, LinkList node) {
    (*tail)->next = node;
    *tail = node;
    node->next = NULL;
}

2. 类似问题变体

按值分类

C
// 将链表分为正数和负数两部分
LinkList splitByValue(LinkList A) {
    LinkList B = (LinkList)malloc(sizeof(LNode));
    LinkList rb = B, p = A;

    while (p->next != NULL) {
        if (p->next->data >= 0) {
            p = p->next;  // 正数保留在 A 中
        } else {
            // 负数移动到 B 中
            LinkList r = p->next;
            p->next = r->next;
            rb->next = r;
            rb = r;
        }
    }
    rb->next = NULL;
    return B;
}

3. 扩展应用场景

多路分解

C
1
2
3
4
// 将链表按位置分为三部分:1,4,7...  2,5,8...  3,6,9...
void splitToThree(LinkList A, LinkList B, LinkList C) {
    // 类似逻辑,按 pos % 3 分类
}

循环链表处理: - 需要特殊处理循环终止条件 - 注意避免无限循环

这种方法体现了链表操作中的经典技巧:原地重排尾插法,在不创建新节点的前提下高效地重新组织数据结构。