C语言链表
C语言链表
一、什么是链表
1.1 链表的定义
链表是非连续、动态的线性数据结构,由若干个节点(Node) 串联而成。
每一个节点由两部分组成
• 数据域:存储我们需要的有效数据(int、char、自定义结构体等)
• 指针域:存储下一个节点的内存地址,负责串联所有节点
简单类比:链表就像一条铁链,每一节铁链(节点)自带数据,同时扣住下一节铁链,最后一节悬空(指向NULL)。
1.2 链表 vs 数组(核心区别)
| 特性 | 数组 | 链表 |
|---|---|---|
| 内存分布 | 连续固定内存 | 分散动态内存,按需申请 |
| 长度限制 | 必须初始化固定长度、易浪费/溢出 | 无固定长度,可无限增删 |
| 插入/删除 | 中间操作需大量元素位移,O(n)复杂度 | 仅需修改指针指向,无需位移 |
| 随机访问 | 支持下标快速访问 | 只能遍历寻找 |
| 内存开销 | 无额外开销 | 每个节点需额外占用指针内存 |
总结使用场景:
• 频繁查询、数据量固定 → 用数组
• 频繁增删、数据量不确定 → 用链表
二、单链表节点定义(固定模板)
C语言中,链表节点依靠结构体+指针实现,这是所有链表操作的基础,通用模板直接背:
// 定义单链表节点结构
typedef struct Node {
// 数据域:存储数据,可根据需求修改类型
int data;
// 指针域:指向同类型的下一个节点
struct Node* next;
} Node;
关键说明:
-
typedef 简化结构体命名,后续直接用Node代替struct Node,代码更简洁
-
next指针必须是struct Node*类型,实现节点自链接
-
最后一个节点的next默认赋值为NULL,代表链表结束
三、单链表全套核心操作(可直接运行)
单链表的核心操作共6个:创建节点、头插、尾插、遍历打印、删除节点、清空链表。
3.1 创建新节点(工具函数)
所有增删操作都需要新建节点,封装成独立函数,避免代码冗余:
// 创建一个新节点,返回节点地址
Node* createNode(int val) {
// 动态申请堆内存
Node* newNode = (Node*)malloc(sizeof(Node));
// 内存分配失败判断(必备容错)
if (newNode == NULL) {
printf("内存分配失败!\n");
exit(1);
}
// 初始化数据域
newNode->data = val;
// 新节点默认无后继,指针置空
newNode->next = NULL;
return newNode;
}
3.2 头插法(链表头部添加节点)
原理:新节点指向原链表第一个节点,头指针指向新节点
优点:速度最快,时间复杂度O(1),无遍历过程
// 头插:在链表头部插入节点
void headInsert(Node** head, int val) {
Node* newNode = createNode(val);
// 新节点后继指向原头节点
newNode->next = *head;
// 头指针更新为新节点
*head = newNode;
}
3.3 尾插法(链表尾部添加节点)
原理:遍历找到链表尾节点,尾节点指针指向新节点
特点:符合日常数据存储逻辑
// 尾插:在链表尾部插入节点
void tailInsert(Node** head, int val) {
Node* newNode = createNode(val);
// 链表为空,直接让新节点作为头节点
if (*head == NULL) {
*head = newNode;
return;
}
// 工作指针,从头节点遍历到尾节点
Node* p = *head;
while (p->next != NULL) {
p = p->next;
}
// 尾节点指向新节点
p->next = newNode;
}
3.4 遍历打印链表
遍历是链表最基础的查询操作,所有链表查找、修改都依赖遍历:
// 遍历并打印链表所有元素
void printList(Node* head) {
// 空链表判断
if (head == NULL) {
printf("链表为空!\n");
return;
}
Node* p = head;
printf("链表元素:");
while (p != NULL) {
printf("%d ", p->data);
// 指针后移,访问下一个节点
p = p->next;
}
printf("\n");
}
3.5 按值删除节点
原理:遍历找到目标节点的前驱节点,修改前驱指针跳过目标节点,最后释放内存
// 删除指定数值的节点
void deleteNode(Node** head, int val) {
// 空链表直接返回
if (*head == NULL) {
printf("链表为空,无法删除!\n");
return;
}
Node* p = *head; // 工作指针
Node* pre = NULL; // 记录前驱节点
// 遍历查找目标节点
while (p != NULL && p->data != val) {
pre = p;
p = p->next;
}
// 未找到目标节点
if (p == NULL) {
printf("未找到目标数据%d!\n", val);
return;
}
// 情况1:删除的是头节点
if (pre == NULL) {
*head = p->next;
}
// 情况2:删除中间/尾节点
else {
pre->next = p->next;
}
// 释放被删除节点的内存
free(p);
printf("成功删除节点:%d\n", val);
}
3.6 清空整个链表
程序结束前必须清空链表,释放堆内存,杜绝内存泄漏:
// 清空链表所有节点
void clearList(Node** head) {
Node* p = *head;
while (p != NULL) {
// 记录下一个节点
Node* temp = p->next;
// 释放当前节点
free(p);
// 指针后移
p = temp;
}
// 头指针置空
*head = NULL;
printf("链表已清空!\n");
}
四、完整测试主函数
int main() {
// 定义空链表,头指针初始化为NULL
Node* head = NULL;
// 尾插数据 1 2 3
tailInsert(&head, 1);
tailInsert(&head, 2);
tailInsert(&head, 3);
printList(head);
// 头插数据 0
headInsert(&head, 0);
printList(head);
// 删除节点2
deleteNode(&head, 2);
printList(head);
// 清空链表
clearList(&head);
printList(head);
return 0;
}
运行结果
链表元素:1 2 3
链表元素:0 1 2 3
成功删除节点:2
链表元素:0 1 3
链表已清空!
链表为空!
五、易错点总结
1. 空指针报错
• 操作链表前必须判断NULL,空链表直接遍历、删除会导致程序崩溃
• 尾节点next必须置空,否则遍历会越界
2. 内存泄漏问题
• malloc申请的堆内存,必须用free释放
• 删除节点、清空链表时,先保存后继节点地址,再释放当前节点
3. 遍历死循环
遍历终止条件必须是p != NULL,不能写p->next != NULL(会漏掉最后一个节点)
六、链表核心复杂度总结
-
头插/头删:O(1) (无需遍历,直接修改指针)
-
尾插/中间插/任意删/查找:O(n) (需要遍历链表)
-
随机访问:不支持,只能从头遍历
结语
链表的核心:指针改变指向,代替数据移动。
相比于数组的死板,链表的动态特性让它成为数据结构的基石,后续栈、队列、哈希表、图的底层实现都离不开链表思想。
浙公网安备 33010602011771号