文章目录
-
-
- 引言:真题背景与核心需求
- 一、真题核心考点分析
- 二、完整代码实现与关键函数解析
-
-
- [1. **链表结构与函数声明**](#1. 链表结构与函数声明)
- [2. 基础辅助函数实现](#2. 基础辅助函数实现)
- [3. 真题核心操作实现](#3. 真题核心操作实现)
-
- (1)链表拆分(split_link)
- (2)链表反转(reverse)
- (3)交替合并(merge)
- [4. 主函数测试流程](#4. 主函数测试流程)
-
- 三、测试结果与复杂度分析
-
-
- [1. 测试用例与输出](#1. 测试用例与输出)
- [2. 复杂度分析](#2. 复杂度分析)
-
- 四、总结与真题启示
-
引言:真题背景与核心需求
2019 年全国硕士研究生招生考试计算机学科专业基础综合(408)的第 41 题,是一道经典的链表操作综合题。其核心需求是对一个单链表完成三项关键操作:拆分、反转与交替合并 ,最终形成一个新的链表结构。
这道题不仅考察对链表基本操作的掌握,更侧重指针操作的熟练度和逻辑思维的严谨性。本文将基于 C 语言,完整实现这道真题的解法,并详细解析每个核心函数的设计思路。
一、真题核心考点分析
要解决这道题,需重点突破三个技术难点,这也是 408 真题考察的核心:
- 链表拆分:将原链表从中间位置拆分为两个独立链表(若长度为奇数,前半部分比后半部分多一个节点)。
- 链表反转:对拆分后的后半段链表进行反转操作。
- 交替合并 :将反转后的后半段链表与前半段链表 "交替插入" 合并(如前半段 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_1 和next_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 真题的核心是指针操作的严谨性和算法效率的优化:
- 拆分用 "快慢指针" 避免先遍历求长度的二次操作,反转用 "三指针法" 实现原地反转,合并用 "提前保存 next" 避免断链,这些都是链表操作的经典技巧。
- 头节点的引入简化了空指针判断,是实际开发中常用的 "统一操作" 思想。
- 边界处理(如空链表、长度 < 2、奇数长度剩余节点)是真题考察的重点,也是代码健壮性的关键。
掌握这些技巧不仅能应对考研真题,也能提升实际开发中对链表数据结构的应用能力。