数据结构:C 语言实现 408 链表真题:解析、拆分、反转与交替合并

文章目录

引言:真题背景与核心需求

2019 年全国硕士研究生招生考试计算机学科专业基础综合(408)的第 41 题,是一道经典的链表操作综合题。其核心需求是对一个单链表完成三项关键操作:拆分、反转与交替合并 ,最终形成一个新的链表结构。

这道题不仅考察对链表基本操作的掌握,更侧重指针操作的熟练度和逻辑思维的严谨性。本文将基于 C 语言,完整实现这道真题的解法,并详细解析每个核心函数的设计思路。

一、真题核心考点分析

要解决这道题,需重点突破三个技术难点,这也是 408 真题考察的核心:

  1. 链表拆分:将原链表从中间位置拆分为两个独立链表(若长度为奇数,前半部分比后半部分多一个节点)。
  2. 链表反转:对拆分后的后半段链表进行反转操作。
  3. 交替合并 :将反转后的后半段链表与前半段链表 "交替插入" 合并(如前半段 L1:1→2→3,反转后半段 L2:5→4,合并后为 1→5→2→4→3)。
    整个实现需保证时间复杂度 O (n)(仅遍历链表常数次)和空间复杂度 O (1)(不额外申请与链表长度相关的空间),符合算法题的效率要求。

二、完整代码实现与关键函数解析

首先定义链表的基本结构与全局函数,再逐一实现核心操作。

1. 链表结构与函数声明

链表节点包含数据域(data)和指针域(next),同时声明创建、遍历、拆分、反转、合并等操作函数。

c 复制代码
#include <stdio.h>
#include <stdlib.h>

typedef int ElementType;  // 数据类型定义,便于后续修改

// 链表节点结构
typedef struct LinkList {
    ElementType data;
    struct LinkList* next;
} Link;

// 函数声明
void create(Link** head_pointer);       // 尾插法创建链表
int traverse(Link* head_pointer);       // 遍历链表并返回长度
void memory_free(Link** head_pointer);  // 释放链表内存
void split_link(Link** head_pointer, Link** split_pointer, int length);  // 拆分链表
void reverse(Link** split_pointer);     // 反转链表
void merge(Link** head_pointer, Link** split_pointer);  // 交替合并链表
2. 基础辅助函数实现
(1)尾插法创建链表(create)

采用 "尾插法" 创建链表,输入数据直至输入99999结束(99999为自定义终止符),并创建一个头节点(数据域设为 - 1,仅用于统一操作,不存储有效数据)。

c 复制代码
void create(Link** head_pointer) {
    if(head_pointer == NULL) return;

    // 创建头节点(统一操作,避免空指针判断)
    Link* head_node = (Link*)malloc(sizeof(Link));
    if(head_node == NULL) {
        printf("头节点创建失败!\n");
        return;
    }
    *head_pointer = head_node;
    head_node->next = NULL;
    head_node->data = -1;

    Link* tail_pointer = head_node;  // 尾指针,始终指向最后一个节点
    ElementType element;
    scanf("%d", &element);

    while(element != 99999) {
        // 创建新节点
        Link* new_node = (Link*)malloc(sizeof(Link));
        if(new_node == NULL) {
            printf("新节点创建失败!\n");
            return;
        }
        new_node->data = element;
        new_node->next = NULL;  // 尾节点的next始终为NULL

        // 尾插:将新节点链接到链表尾部
        tail_pointer->next = new_node;
        tail_pointer = new_node;  // 更新尾指针

        scanf("%d", &element);
    }
    printf("链表创建完成!\n");
}
(2)遍历链表(traverse)

遍历链表并打印节点数据,同时统计有效节点个数(跳过头节点),用于后续拆分操作的长度判断。

c 复制代码
int traverse(Link* head_pointer) {
    int count = 0;
    if(head_pointer == NULL) {
        printf("链表为空!\n");
        return 0;
    }

    // 若存在头节点(data=-1),则从下一个节点开始遍历
    Link* start = (head_pointer->data == -1) ? head_pointer->next : head_pointer;
    printf("链表内容:");
    while(start != NULL) {
        printf("%d", start->data);
        count++;
        start = start->next;
        if(start != NULL) printf(" ");  // 节点间加空格,优化输出格式
    }
    printf("\n");
    return count;  // 返回有效节点个数
}
(3)内存释放(memory_free)

遍历链表并逐个释放节点内存,避免内存泄漏,最后将头指针置空(防止野指针)。

c 复制代码
void memory_free(Link** head_pointer) {
    if(head_pointer == NULL || *head_pointer == NULL) {
        printf("链表为空,无需释放!\n");
        return;
    }

    Link* current = *head_pointer;
    while(current != NULL) {
        Link* temp = current;  // 临时保存当前节点
        current = current->next;  // 指针后移
        free(temp);  // 释放当前节点
    }
    *head_pointer = NULL;  // 头指针置空,避免野指针
    printf("内存释放完成!\n");
}
3. 真题核心操作实现
(1)链表拆分(split_link)

核心思路 :采用快慢指针法找到链表中间节点,将链表拆分为前半段(原链表头指针指向)和后半段(新指针split_pointer指向)。
快指针(fast_p) :每次移动 2 个节点。
慢指针(slow_p) :每次移动 1 个节点。

当快指针到达链表尾部时,慢指针恰好指向前半段的尾节点,其next即为后半段的头节点。

c 复制代码
void split_link(Link** head_pointer, Link** split_pointer, int length) {
    // 边界判断:链表为空或长度<2,无需拆分
    if(head_pointer == NULL || length < 2) {
        printf("链表为空或长度不足,无法拆分!\n");
        return;
    }

    // 快慢指针均从第一个有效节点开始(跳过头节点)
    Link* fast_p = (*head_pointer)->next;
    Link* slow_p = (*head_pointer)->next;

    // 快指针移动:每次走2步,慢指针走1步
    while(fast_p->next != NULL) {
        fast_p = fast_p->next;
        // 若快指针下一步为空(偶数长度),则停止(避免越界)
        if(fast_p->next == NULL) break;
        fast_p = fast_p->next;
        slow_p = slow_p->next;
    }

    // 拆分:后半段链表头为slow_p->next,前半段尾节点next置空(断链)
    *split_pointer = slow_p->next;
    slow_p->next = NULL;  // 前半段链表终止
    printf("链表拆分完成!\n");
}
(2)链表反转(reverse)

核心思路 :采用迭代法(三指针法) 反转链表,无需额外申请空间,时间复杂度 O (n)。
current_p :当前待处理的节点。
previous_p :当前节点的前一个节点(初始为 NULL,作为反转后链表的尾节点)。
next_p:保存当前节点的下一个节点(防止断链后丢失后续节点)。

c 复制代码
void reverse(Link** split_pointer) {
    // 边界判断:链表为空
    if(split_pointer == NULL || *split_pointer == NULL) {
        printf("待反转链表为空!\n");
        return;
    }

    Link* previous_p = NULL;  // 前一个节点(初始为NULL)
    Link* current_p = *split_pointer;  // 当前节点(从后半段头节点开始)
    Link* next_p = NULL;      // 保存下一个节点

    while(current_p != NULL) {
        next_p = current_p->next;  // 保存下一个节点
        current_p->next = previous_p;  // 反转当前节点的指针(指向前一个节点)
        previous_p = current_p;  // 前指针后移
        current_p = next_p;      // 当前指针后移(使用保存的next_p)
    }

    // 反转完成后,previous_p指向新的头节点
    *split_pointer = previous_p;
    printf("链表反转完成!\n");
}
(3)交替合并(merge)

核心思路 :将反转后的后半段链表(L2)交替插入前半段链表(L1)中,即 L1 的第 1 个节点后接 L2 的第 1 个节点,再接 L1 的第 2 个节点,以此类推。

next_p_1next_p_2 分别保存 L1 和 L2 当前节点的下一个节点(防止断链)。

last_node记录最后一个处理的节点,若 L2 长度更长(奇数长度时),则将剩余节点直接接在last_node后。

c 复制代码
void merge(Link** head_pointer, Link** split_pointer) {
    // 边界判断:原链表或后半段链表为空
    if(head_pointer == NULL || *head_pointer == NULL || split_pointer == NULL || *split_pointer == NULL) {
        printf("合并失败:链表为空!\n");
        return;
    }

    Link* L1 = (*head_pointer)->next;  // L1:前半段有效节点(跳过头节点)
    Link* L2 = *split_pointer;         // L2:反转后的后半段链表
    Link* next_p_1 = NULL;             // 保存L1当前节点的下一个节点
    Link* next_p_2 = NULL;             // 保存L2当前节点的下一个节点
    Link* last_node = NULL;            // 记录最后一个处理的节点(用于处理剩余节点)

    while(L1 != NULL && L2 != NULL) {
        // 1. 保存下一个节点(防止断链)
        next_p_1 = L1->next;
        next_p_2 = L2->next;

        // 2. 交替链接:L1当前节点 → L2当前节点 → L1下一个节点
        L1->next = L2;
        L2->next = next_p_1;

        // 3. 更新尾节点(当前L2节点为最后处理的节点)
        last_node = L2;

        // 4. 指针后移
        L1 = next_p_1;
        L2 = next_p_2;
    }

    // 若L2有剩余节点(原链表长度为奇数时),直接接在尾节点后
    if(L2 != NULL) {
        last_node->next = L2;
    }

    // 合并完成后,split_pointer置空(避免重复操作)
    *split_pointer = NULL;
    printf("链表合并完成!\n");
}
4. 主函数测试流程

主函数按 "创建→遍历→拆分→反转→合并→释放" 的流程测试所有功能,模拟真题的完整操作步骤。

c 复制代码
int main() {   
    Link* head = NULL;       // 原链表头指针
    Link* split = NULL;      // 拆分后的后半段链表头指针

    // 1. 创建链表
    printf("请输入链表数据(以99999结束):\n");
    create(&head);

    // 2. 遍历链表并获取长度
    int length = traverse(head);
    printf("链表长度:%d\n", length);

    // 3. 拆分链表
    split_link(&head, &split, length);
    printf("拆分后前半段:");
    traverse(head);
    printf("拆分后后半段:");
    traverse(split);

    // 4. 反转后半段链表
    reverse(&split);
    printf("反转后后半段:");
    traverse(split);

    // 5. 交替合并链表
    merge(&head, &split);
    printf("合并后最终链表:");
    traverse(head);

    // 6. 释放内存
    memory_free(&head);
    return 0;
}

三、测试结果与复杂度分析

1. 测试用例与输出

以输入1 2 3 4 5 99999(链表长度 5)为例,输出如下:

plaintext 复制代码
请输入链表数据(以99999结束):
1 2 3 4 5 99999
链表创建完成!
链表内容:1 2 3 4 5
链表长度:5
链表拆分完成!
拆分后前半段:链表内容:1 2 3
拆分后后半段:链表内容:4 5
链表反转完成!
反转后后半段:链表内容:5 4
链表合并完成!
合并后最终链表:链表内容:1 5 2 4 3
内存释放完成!
结果符合预期:拆分后前半段1→2→3、后半段4→5,反转后半段为5→4,合并后为1→5→2→4→3。
2. 复杂度分析

时间复杂度 :O (n)。拆分、反转、合并操作均仅遍历链表 1 次,遍历操作也仅遍历 1 次,整体为线性时间。
空间复杂度:O (1)。仅使用常数个额外指针(快慢指针、三指针等),未申请与链表长度相关的空间。

四、总结与真题启示

这道 408 真题的核心是指针操作的严谨性和算法效率的优化:

  1. 拆分用 "快慢指针" 避免先遍历求长度的二次操作,反转用 "三指针法" 实现原地反转,合并用 "提前保存 next" 避免断链,这些都是链表操作的经典技巧。
  2. 头节点的引入简化了空指针判断,是实际开发中常用的 "统一操作" 思想。
  3. 边界处理(如空链表、长度 < 2、奇数长度剩余节点)是真题考察的重点,也是代码健壮性的关键。

掌握这些技巧不仅能应对考研真题,也能提升实际开发中对链表数据结构的应用能力。

相关推荐
APIshop15 小时前
阿里巴巴 1688 API 接口深度解析:商品详情与按图搜索商品(拍立淘)实战指南
1024程序员节
芙蓉王真的好115 小时前
VSCode 配置 Dubbo 超时与重试:application.yml 配置的详细步骤
1024程序员节
默 语16 小时前
MySQL中的数据去重,该用DISTINCT还是GROUP BY?
java·数据库·mysql·distinct·group by·1024程序员节·数据去重
重生之我是Java开发战士16 小时前
【Java EE】Spring Web MVC入门:综合实践与架构设计
1024程序员节
Echoo华地16 小时前
GitLab社区版日志rotate失败的问题
1024程序员节
asfdsfgas17 小时前
华硕 Armoury Crate 安装卡 50% 不动:清理安装缓存文件的解决步骤
1024程序员节
安冬的码畜日常18 小时前
【JUnit实战3_10】第六章:关于测试的质量(上)
测试工具·junit·单元测试·测试覆盖率·1024程序员节·junit5
安冬的码畜日常19 小时前
【JUnit实战3_11】第六章:关于测试的质量(下)
junit·单元测试·tdd·1024程序员节·bdd·变异测试
zhangzhangkeji20 小时前
UE5 蓝图-11:本汽车蓝图的事件图表,汽车拆分事件,染色事件(绿蓝黄青)。
ue5·1024程序员节