数据结构: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、奇数长度剩余节点)是真题考察的重点,也是代码健壮性的关键。

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

相关推荐
学传打活3 天前
【边打字.边学昆仑正义文化】_进阶篇5 宇宙星系的运转(续3)
微信公众平台·1024程序员节·汉字·昆仑正义文化
学传打活8 天前
【边打字.边学昆仑正义文化】_进阶篇5 宇宙星系的运转(续1)
微信公众平台·1024程序员节·汉字·昆仑正义文化
fangzhanpeng16816 天前
快速掌握C#语言基础知识点(1.基础数据类型)
开发语言·c#·1024程序员节
无己心18 天前
面向多平台 AI 搜索引擎的适配层架构设计
前端·人工智能·搜索引擎·ai·1024程序员节
u01030552724 天前
AI驱动的安卓UI自动生成功能实现
人工智能·1024程序员节
u01030552725 天前
长株潭智能体赋能腾讯元器链接共享
1024程序员节
云贝贝贝1 个月前
OceanBase认证体系介绍与备考指南
运维·服务器·oceanbase·1024程序员节
u0103055271 个月前
Java图像处理实战指南
人工智能·1024程序员节
u0103055271 个月前
Java AWT鼠标事件全解析
人工智能·1024程序员节
u0103055271 个月前
Java实用类应用精讲
人工智能·1024程序员节