跳转至

AC206-将一个单链表 {a₁, b₁, a₂, b₂, ……, aₙ, bₙ} 拆分成两个单链表:第一个链表保持 a₁ 到 aₙ 的原始顺序,第二个链表中 b 的元素按逆序排列

代码实现

C
// 函数 split:将单链表 {a1,b1,a2,b2,...,an,bn} 拆分成两个链表
// 第一个链表保持 {a1,a2,...,an} 的顺序
// 第二个链表为 {bn,bn-1,...,b1} 的逆序
LinkList split(LinkList A) {
    // 创建新链表 B 的头节点
    LinkList B = (LinkList)malloc(sizeof(LNode));
    B->next = NULL; // 初始化链表 B 为空

    LinkList p = A; // p 指针用于遍历原链表 A
    int pos = 1;    // 位置计数器,用于判断当前节点是奇数位还是偶数位

    // 遍历原链表 A,处理每个节点
    while (p->next != NULL) {
        // 如果是奇数位置(a1, a2, ..., an),保留在此链表中
        if (pos % 2 == 1) {
            p = p->next; // 移动指针,不进行任何操作
        } else {
            // 如果是偶数位置(b1, b2, ..., bn),将其移动到链表 B
            LinkList r = p->next;      // r 指向要移动的节点
            p->next = r->next;         // 从原链表 A 中删除该节点
            r->next = B->next;         // 将该节点插入到链表 B 的头部
            B->next = r;               // 更新链表 B 的头指针
            // 注意:此时 p 指针不移动,因为要继续处理新连接的节点
        }
        pos++; // 位置计数器加1
    }

    return B; // 返回新创建的链表 B
}

代码逻辑解析

1. 问题理解

原始链表结构:{a1, b1, a2, b2, ..., an, bn}(共 2n 个节点) - 目标1:链表 A 保持 {a1, a2, ..., an} 的顺序 - 目标2:链表 B 存储 {bn, bn-1, ..., b1} 的逆序

2. 算法策略

  • 使用位置计数器 pos 来区分奇数位(a系列)和偶数位(b系列)
  • 奇数位节点保留在原链表中
  • 偶数位节点从原链表中删除,并采用头插法插入到新链表 B 中

3. 头插法的关键作用

由于采用头插法插入到链表 B: - 第一个被移动的 b1 节点会成为 B 链表的最后一个节点 - 最后一个被移动的 bn 节点会成为 B 链表的第一个节点 - 从而实现了 {bn, bn-1, ..., b1} 的逆序效果

4. 指针操作详解

Text Only
原链表 A: ... -> p -> r -> next_node -> ...
移动后:   ... -> p -> next_node -> ... (r 被移动到 B)

时间复杂度分析

  • 链表遍历:需要遍历原链表的一次,共 2n 个节点,时间复杂度为 O(2n) = O(n)
  • 节点操作:每次节点的删除和插入操作都是常数时间 O(1)
  • 总操作次数:最多进行 n 次节点移动操作

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


空间复杂度分析

  • 辅助空间:只创建了一个新链表的头节点 B,占用常数空间 O(1)
  • 指针变量:使用了常量级别的额外指针变量 p, r, pos

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


较难理解部分的说明

为什么采用头插法能实现逆序?

让我们通过具体例子来说明:

假设原链表为:A -> a1 -> b1 -> a2 -> b2 -> a3 -> b3 -> NULL

处理过程: 1. pos=1:a1(奇数位)→ 保留在 A 中 2. pos=2:b1(偶数位)→ 移动到 B,B: B -> b1 -> NULL 3. pos=3:a2(奇数位)→ 保留在 A 中
4. pos=4:b2(偶数位)→ 移动到 B,B: B -> b2 -> b1 -> NULL 5. pos=5:a3(奇数位)→ 保留在 A 中 6. pos=6:b3(偶数位)→ 移动到 B,B: B -> b3 -> b2 -> b1 -> NULL

图解说明

Text Only
初始状态:
A: A_head -> a1 -> b1 -> a2 -> b2 -> a3 -> b3 -> NULL
B: B_head -> NULL

第一次移动 b1:
A: A_head -> a1 -> a2 -> b2 -> a3 -> b3 -> NULL
B: B_head -> b1 -> NULL

第二次移动 b2:
A: A_head -> a1 -> a2 -> a3 -> b3 -> NULL  
B: B_head -> b2 -> b1 -> NULL

第三次移动 b3:
A: A_head -> a1 -> a2 -> a3 -> NULL
B: B_head -> b3 -> b2 -> b1 -> NULL

最终结果: - 链表 A:{a1, a2, a3} - 链表 B:{b3, b2, b1}(即 {b3, b2, b1} 的逆序)


延伸知识点

1. 类似问题变体

变体1:保持 b 系列的原始顺序

C
// 如果要求 B 链表为 {b1, b2, ..., bn} 的原始顺序
// 可以先按逆序存储,最后再进行一次链表反转

变体2:三等分链表

C
1
2
3
4
// 将 {a1,b1,c1,a2,b2,c2,...,an,bn,cn} 拆分为三个链表
if (pos % 3 == 1) { /* a 系列 */ }
else if (pos % 3 == 2) { /* b 系列 */ }
else { /* c 系列,采用尾插法保持顺序 */ }

2. 链表操作技巧

头插法 vs 尾插法: - 头插法:插入新节点到链表头部,实现逆序效果 - 尾插法:插入新节点到链表尾部,保持原有顺序

双指针技巧

C
1
2
3
4
5
6
7
// 另一种实现方式:使用快慢指针
LinkList slow = A, fast = A;
while (fast && fast->next) {
    slow = slow->next;
    fast = fast->next->next;
}
// slow 指向中间位置

3. 实际应用场景

  • 数据分组处理:将交替存储的数据分离到不同的处理队列
  • 缓存管理:将访问频率不同的数据分离存储
  • 任务调度:将不同类型的任务分配到不同的执行队列

4. 错误处理和边界情况

在实际应用中还需要考虑:

C
LinkList split(LinkList A) {
    // 边界检查
    if (A == NULL || A->next == NULL) {
        return NULL;
    }

    // 内存分配检查
    LinkList B = (LinkList)malloc(sizeof(LNode));
    if (B == NULL) {
        return NULL; // 内存分配失败
    }

    // ... 原有逻辑 ...

    return B;
}

这个算法体现了链表操作中原地修改头插法逆序的经典技巧,是链表算法中的重要知识点。