📖 推荐阅读:《Yocto项目实战教程:高效定制嵌入式Linux系统》
🎥 更多学习视频请关注 B 站:嵌入式Jerry
数据结构基础练习 | 单链表操作全套实战与复盘
前言
单链表是数据结构与算法学习的核心内容之一,不仅是算法题的高频考点,更是C语言面试和实际工程开发的基础能力。通过扎实掌握单链表的各种基本操作,可以极大提升你的编程思维与指针理解能力。本练习代码涵盖了链表创建、打印、反转、插入(头、尾、任意位置)、删除(指定位置)等典型功能,每一个操作都对应着真实项目和面试中的核心用法。
一、链表的核心用途和实战意义
- 高效插入/删除:链表结构在已知节点指针的情况下,插入和删除都能做到O(1)时间复杂度,优于数组的O(n)。
- 动态内存管理:链表在插入、删除节点时无需移动大量元素,内存利用率高,适合实现队列、栈、缓存等结构。
- 算法面试基础:链表题目能全面考查指针运算、内存管理、边界处理,是算法笔试/面试绕不开的内容。
- 工程开发常见场景:如操作系统进程链表、LRU缓存、哈希拉链法、邻接表等,都需要链表的熟练应用。
二、练习意图与目标
- 基础夯实:通过自写链表各类基础操作,打牢指针和内存操作的基本功。
- 边界覆盖:全面训练空链表、单节点、头/尾插入、越界等特殊场景,强化代码健壮性。
- 代码复用与归纳:将常用链表操作形成模板,积累个人算法/工程工具库。
- 实战思维提升:每个操作不仅为算法题做准备,更贴合实际开发需求,提升综合能力。
三、完整代码与注释
#include <stdio.h>
#include <stdlib.h>
// 单链表节点定义
typedef struct ListNode {
int val;
struct ListNode* next;
} ListNode;
// 创建链表(数组转链表)
ListNode* createList(int* arr, int len) {
if(len <= 0)
return NULL;
int i = 0;
ListNode * head = malloc(sizeof(ListNode));
if (!head) return NULL;
head->val = arr[i++];
ListNode * tmp = head;
while(--len){
ListNode * nt = malloc(sizeof(ListNode));
if (!nt) return head; // 分配失败提前返回
nt->val = arr[i++];
tmp->next = nt;
tmp = nt;
}
tmp->next = NULL;
return head;
}
// 打印链表
void printList(ListNode* head) {
ListNode* p = head;
while (p) {
printf("%d", p->val);
if (p->next) printf(" -> ");
p = p->next;
}
printf("\n");
}
// 释放链表
void freeList(ListNode* head) {
while (head) {
ListNode* tmp = head;
head = head->next;
free(tmp);
}
}
// 链表反转
ListNode* reverseList(ListNode* head) {
ListNode * pre = NULL;
while(head){
ListNode * nt = head->next;
head->next = pre;
pre = head;
head = nt;
}
return pre;
}
// 头插法
ListNode* insertHead(ListNode* head, int val) {
ListNode* node = malloc(sizeof(ListNode));
node->val = val;
node->next = head;
return node;
}
// 尾插法
ListNode* insertTail(ListNode* head, int val) {
ListNode* node = malloc(sizeof(ListNode));
node->val = val;
node->next = NULL;
if(head==NULL)
return node;
ListNode* tmp = head;
while(tmp->next)
tmp = tmp->next;
tmp->next = node;
return head;
}
// 指定位置插入
ListNode* insertAt(ListNode* head, int pos, int val) {
ListNode * node = malloc(sizeof(ListNode));
node->val = val;
node->next = NULL;
if(pos == 0){
node->next = head;
return node;
}
ListNode* pre = head;
ListNode* nt = head->next;
int i = 1;
while(i < pos && nt){
pre = pre->next;
nt = nt->next;
i++;
}
pre->next = node;
node->next = nt;
return head;
}
// 指定位置删除
ListNode* deleteAt(ListNode* head, int pos) {
if(pos == 0 && head){
ListNode* nt = head->next;
free(head);
return nt;
}
ListNode* pre = head;
ListNode* nt = head ? head->next : NULL;
int i = 1;
while(i < pos && nt){
pre = pre->next;
nt = nt->next;
i++;
}
if(nt){
pre->next = nt->next;
free(nt);
}
return head;
}
// main函数测试
int main() {
int arr[] = {1,2,3,4,5};
ListNode* head = createList(arr, 5);
printf("原链表: ");
printList(head);
// 头插
head = insertHead(head, 11);
printf("头插11: ");
printList(head);
// 尾插
head = insertTail(head, 12);
printf("尾插12: ");
printList(head);
// 指定位置插入
head = insertAt(head, 5, 13);
printf("在第5个位置插入13: ");
printList(head);
// 删除第4个节点
head = deleteAt(head, 4);
printf("删除第4个节点: ");
printList(head);
// 反转链表
head = reverseList(head);
printf("反转后链表: ");
printList(head);
freeList(head);
return 0;
}
四、各操作的核心用途与练习意图
1. 链表创建与销毁
- 用途:把数据结构与C的内存管理相结合,理解链表“动态生成与释放”的全过程。
- 意图:培养“用完即释放”的习惯,防止内存泄漏,理解malloc/free的本质。
2. 打印链表
- 用途:调试和可视化链表内容,验证操作正确性。
- 意图:锻炼“调试-输出-验证”能力,为复杂链表题目做准备。
3. 头插/尾插/指定位置插入
- 用途:快速在链表任何位置插入节点,实现队列、栈、哈希表拉链等实际结构。
- 意图:熟悉指针操作,学会处理各种边界(如头/尾/越界/空链表等)。
4. 指定位置删除
- 用途:灵活删除链表任意节点,贴合实际工程与算法需求(如LRU淘汰、队列出队等)。
- 意图:练习节点定位、指针连接与内存释放,理解链表动态修改的全过程。
5. 链表反转
- 用途:算法面试高频考点,实际工程中如链表逆序打印、撤销操作、历史栈管理等。
- 意图:掌握三指针法,体会指针移动的精髓,构建更复杂的链表操作模板。
五、实战应用举例
- 队列/栈底层实现:队列常用尾插头删,栈常用头插头删,链表轻松支持。
- 哈希表拉链法:每个桶挂一条链表,插入删除查找都用链表实现。
- 操作系统进程/任务管理:任务链表、定时器链表等动态插入/删除频繁。
- 缓存与调度:如LRU缓存、消息队列、任务池等。
六、学习与复盘建议
- 多写多练,手写最扎实:每个函数都动手写一遍,绝不抄现成代码。
- 注重边界和异常处理:空链表、单节点、越界、分配失败都要测一测。
- 练习模板归纳:总结每种操作的固定模板,形成个人“代码片段库”。
- 与实际项目对照:思考这些基本操作在真实工程(如队列、缓存)中的应用。
- 持续优化和补充:尝试加上链表查找、环检测、合并、排序等进阶功能。
七、结语
单链表操作的熟练掌握,是通往高级数据结构、算法和工程开发的基础门槛。
建议将本套练习反复复习、举一反三,结合实际项目和LeetCode题目进行巩固,逐步形成自己的数据结构体系。
遇到问题及时查阅/请教,写完每一题都要理解透彻,只有这样,才能在面试和实战中从容应对各种链表问题!