一、双向链表核心概念
1.1 双向链表的结构特点
- 每个节点包含三部分:数据域 (存储实际数据)、前驱指针 prev (指向当前节点的前一个节点)、后继指针 next(指向当前节点的后一个节点)。
- 支持两种遍历方式:从头到尾(通过 next 指针)、从尾到头(通过 prev 指针)。
- 插入 / 删除操作时,需同时维护 prev 和 next 指针的指向,确保链表的完整性。
1.2 核心设计思路
本文实现的双向链表支持自定义数据类型(以学生信息为例),包含以下核心功能:
- 链表初始化与销毁
- 头部 / 尾部 / 指定位置插入
- 从头到尾 / 从尾到头遍历
- 按关键字查找、修改、删除
- 链表反转
- 判空、获取长度
二、头文件定义(doulink.h)
首先定义链表的节点结构、链表管理结构及相关枚举 / 宏定义,统一封装到头文件中,方便工程复用。
#ifndef DOULINK_H
#define DOULINK_H
// 自定义数据类型:学生信息(可根据需求修改)
typedef struct {
char name[20]; // 姓名
int age; // 年龄
char sex; // 性别('M'男/'F'女)
int score; // 成绩
} DATATYPE;
// 双向链表节点结构
typedef struct DouLinkNode {
DATATYPE data; // 数据域
struct DouLinkNode* prev; // 前驱指针
struct DouLinkNode* next; // 后继指针
} DouLinkNode;
// 链表管理结构(存储链表头节点和长度)
typedef struct {
DouLinkNode* head; // 头节点指针
int clen; // 链表实际节点个数
} DouLinkList;
// 遍历方向枚举
typedef enum {
SHOW_FORWARD = 0, // 从头到尾
SHOW_BACKWARD // 从尾到头
} SHOW_DIR;
// 函数声明
// 1. 创建双向链表(初始化)
DouLinkList* CreateDouLinkList();
// 2. 头部插入节点
int InsertHeadDouLinkList(DouLinkList* dl, DATATYPE* data);
// 3. 尾部插入节点
int InsertTailDouLinkList(DouLinkList* dl, DATATYPE* data);
// 4. 指定位置插入节点(pos从0开始)
int InsertPosDouLinkList(DouLinkList* dl, DATATYPE* data, int pos);
// 5. 遍历链表
int ShowDoulinkList(DouLinkList* dl, SHOW_DIR dir);
// 6. 按姓名查找数据(返回数据地址)
DATATYPE* FindDouLinkList(DouLinkList* dl, char* name);
// 7. 按姓名修改数据
int ModifyDouLinkList(DouLinkList* dl, char* name, DATATYPE* newdata);
// 8. 按姓名删除节点
int DeleteDoulinkList(DouLinkList* dl, char* name);
// 9. 反转链表
int ReverseDouLinkList(DouLinkList* dl);
// 10. 获取链表长度
int GetsizeDouLinkList(DouLinkList* dl);
// 11. 判断链表是否为空
int IsEmptyDouLinkList(DouLinkList* dl);
// 12. 销毁链表(释放内存)
int DestroyDouLinkList(DouLinkList** dl);
// 内部辅助函数:按姓名查找节点(返回节点指针)
static DouLinkNode* FindDouLinkList2(DouLinkList* dl, char* name);
#endif // DOULINK_H
三、核心功能实现(doulink.c)
基于头文件的声明,实现双向链表的所有功能,重点关注指针操作的正确性和内存安全。
3.1 链表初始化(CreateDouLinkList)
创建链表管理结构,初始化头节点为 NULL,长度为 0。
#include "doulink.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
DouLinkList* CreateDouLinkList() {
DouLinkList* dl = (DouLinkList*)malloc(sizeof(DouLinkList));
if (NULL == dl) {
printf("CreateDouLinkList: 内存分配失败!\n");
return NULL;
}
dl->head = NULL; // 初始时无节点,头指针为NULL
dl->clen = 0; // 链表长度为0
return dl;
}
3.2 节点插入操作
插入操作是链表的核心,需处理头部、尾部、指定位置三种场景,确保指针指向正确。
3.2.1 头部插入(InsertHeadDouLinkList)
新节点成为新的头节点,原头节点的 prev 指针指向新节点。
int InsertHeadDouLinkList(DouLinkList* dl, DATATYPE* data) {
if (dl == NULL || data == NULL) {
printf("InsertHeadDouLinkList: 参数非法!\n");
return 1;
}
// 创建新节点
DouLinkNode* newnode = (DouLinkNode*)malloc(sizeof(DouLinkNode));
if (NULL == newnode) {
printf("InsertHeadDouLinkList: 节点内存分配失败!\n");
return 1;
}
// 复制数据到新节点
memcpy(&newnode->data, data, sizeof(DATATYPE));
newnode->prev = NULL; // 头部节点的前驱为NULL
newnode->next = NULL;
// 新节点成为头节点
newnode->next = dl->head;
if (NULL != dl->head) { // 若原链表非空,原头节点的prev指向新节点
dl->head->prev = newnode;
}
dl->head = newnode;
dl->clen++; // 长度+1
return 0;
}
3.2.2 尾部插入(InsertTailDouLinkList)
遍历到链表尾部,新节点的 prev 指向尾节点,尾节点的 next 指向新节点。
int InsertTailDouLinkList(DouLinkList* dl, DATATYPE* data) {
if (dl == NULL || data == NULL) {
printf("InsertTailDouLinkList: 参数非法!\n");
return 1;
}
// 若链表为空,直接调用头部插入
if (IsEmptyDouLinkList(dl)) {
return InsertHeadDouLinkList(dl, data);
}
// 创建新节点
DouLinkNode* newnode = (DouLinkNode*)malloc(sizeof(DouLinkNode));
if (NULL == newnode) {
printf("InsertTailDouLinkList: 节点内存分配失败!\n");
return 1;
}
memcpy(&newnode->data, data, sizeof(DATATYPE));
newnode->prev = NULL;
newnode->next = NULL;
// 遍历到尾部节点
DouLinkNode* tmp = dl->head;
while (tmp->next != NULL) {
tmp = tmp->next;
}
// 链接新节点
newnode->prev = tmp;
tmp->next = newnode;
dl->clen++; // 长度+1
return 0;
}
3.2.3 指定位置插入(InsertPosDouLinkList)
先遍历到目标位置的前驱节点,再插入新节点,同时维护前后节点的指针。
int InsertPosDouLinkList(DouLinkList* dl, DATATYPE* data, int pos) {
if (dl == NULL || data == NULL) {
printf("InsertPosDouLinkList: 参数非法!\n");
return 1;
}
int size = GetsizeDouLinkList(dl);
// 检查位置合法性(pos范围:0~size)
if (pos < 0 || pos > size) {
printf("InsertPosDouLinkList: 插入位置越界!\n");
return 1;
}
// 特殊场景:头部插入
if (pos == 0) {
return InsertHeadDouLinkList(dl, data);
}
// 特殊场景:尾部插入
if (pos == size) {
return InsertTailDouLinkList(dl, data);
}
// 遍历到pos的前驱节点(pos-1位置)
DouLinkNode* tmp = dl->head;
for (int i = 0; i < pos - 1; i++) {
tmp = tmp->next;
}
// 创建新节点
DouLinkNode* newnode = (DouLinkNode*)malloc(sizeof(DouLinkNode));
if (NULL == newnode) {
printf("InsertPosDouLinkList: 节点内存分配失败!\n");
return 1;
}
memcpy(&newnode->data, data, sizeof(DATATYPE));
newnode->prev = NULL;
newnode->next = NULL;
// 插入新节点:维护四个指针
newnode->prev = tmp;
newnode->next = tmp->next;
tmp->next->prev = newnode;
tmp->next = newnode;
dl->clen++; // 长度+1
return 0;
}
3.3 链表遍历(ShowDoulinkList)
支持双向遍历:从头到尾(通过 next 指针)、从尾到头(先找到尾节点,再通过 prev 指针)。
int ShowDoulinkList(DouLinkList* dl, SHOW_DIR dir) {
if (dl == NULL || IsEmptyDouLinkList(dl)) {
printf("ShowDoulinkList: 链表为空!\n");
return 1;
}
DouLinkNode* tmp = dl->head;
if (SHOW_FORWARD == dir) {
printf("=== 从头到尾遍历链表 ===\n");
while (tmp != NULL) {
printf("姓名:%s | 年龄:%d | 性别:%c | 成绩:%d\n",
tmp->data.name, tmp->data.age,
tmp->data.sex, tmp->data.score);
tmp = tmp->next;
}
} else {
printf("=== 从尾到头遍历链表 ===\n");
// 先遍历到尾节点
while (tmp->next != NULL) {
tmp = tmp->next;
}
// 从尾节点反向遍历
while (tmp != NULL) {
printf("姓名:%s | 年龄:%d | 性别:%c | 成绩:%d\n",
tmp->data.name, tmp->data.age,
tmp->data.sex, tmp->data.score);
tmp = tmp->prev;
}
}
printf("\n");
return 0;
}
3.4 查找与修改操作
3.4.1 按姓名查找(FindDouLinkList / FindDouLinkList2)
提供两个查找函数:一个返回数据地址(供外部使用),一个返回节点指针(供内部删除 / 修改使用)。
// 外部接口:返回数据地址
DATATYPE* FindDouLinkList(DouLinkList* dl, char* name) {
if (dl == NULL || name == NULL || IsEmptyDouLinkList(dl)) {
return NULL;
}
DouLinkNode* tmp = FindDouLinkList2(dl, name);
return tmp ? &tmp->data : NULL;
}
// 内部辅助函数:返回节点指针(static限制作用域)
static DouLinkNode* FindDouLinkList2(DouLinkList* dl, char* name) {
if (dl == NULL || name == NULL || IsEmptyDouLinkList(dl)) {
return NULL;
}
DouLinkNode* tmp = dl->head;
// 遍历链表匹配姓名(strcmp比较字符串)
while (tmp != NULL) {
if (0 == strcmp(tmp->data.name, name)) {
return tmp; // 找到返回节点指针
}
tmp = tmp->next;
}
return NULL; // 未找到返回NULL
}
3.4.2 按姓名修改(ModifyDouLinkList)
先通过查找函数找到目标数据,再用 memcpy 覆盖修改。
int ModifyDouLinkList(DouLinkList* dl, char* name, DATATYPE* newdata) {
if (dl == NULL || name == NULL || newdata == NULL) {
printf("ModifyDouLinkList: 参数非法!\n");
return 1;
}
DATATYPE* olddata = FindDouLinkList(dl, name);
if (NULL == olddata) {
printf("ModifyDouLinkList: 未找到姓名为【%s】的节点!\n", name);
return 1;
}
// 覆盖旧数据
memcpy(olddata, newdata, sizeof(DATATYPE));
printf("ModifyDouLinkList: 节点修改成功!\n");
return 0;
}
3.5 节点删除(DeleteDoulinkList)
删除操作需处理三种场景:头节点、尾节点、中间节点,删除后释放节点内存,避免内存泄漏。
int DeleteDoulinkList(DouLinkList* dl, char* name) {
if (dl == NULL || name == NULL || IsEmptyDouLinkList(dl)) {
printf("DeleteDoulinkList: 参数非法或链表为空!\n");
return 1;
}
// 找到要删除的节点
DouLinkNode* tmp = FindDouLinkList2(dl, name);
if (NULL == tmp) {
printf("DeleteDoulinkList: 未找到姓名为【%s】的节点!\n", name);
return 1;
}
// 场景1:删除头节点
if (tmp == dl->head) {
dl->head = dl->head->next; // 新头节点为原头节点的下一个
if (dl->head != NULL) { // 若链表非空,新头节点的prev置为NULL
dl->head->prev = NULL;
}
}
// 场景2:删除尾节点
else if (tmp->next == NULL) {
tmp->prev->next = NULL; // 尾节点的前驱节点next置为NULL
}
// 场景3:删除中间节点
else {
tmp->prev->next = tmp->next; // 前驱节点的next指向后继节点
tmp->next->prev = tmp->prev; // 后继节点的prev指向前驱节点
}
// 释放删除节点的内存
free(tmp);
tmp = NULL;
dl->clen--; // 长度-1
printf("DeleteDoulinkList: 节点删除成功!\n");
return 0;
}
3.6 链表反转(ReverseDouLinkList)
通过调整每个节点的 prev 和 next 指针方向,实现链表反转,时间复杂度 O (n)。
int ReverseDouLinkList(DouLinkList* dl) {
if (dl == NULL || GetsizeDouLinkList(dl) < 2) {
printf("ReverseDouLinkList: 链表为空或节点数小于2,无需反转!\n");
return 1;
}
DouLinkNode* curr = dl->head; // 当前节点
DouLinkNode* prev = NULL; // 前驱节点(初始为NULL)
DouLinkNode* next = NULL; // 后继节点(临时存储)
while (curr != NULL) {
next = curr->next; // 保存当前节点的下一个节点
curr->next = prev; // 反转当前节点的next指针(指向原前驱)
curr->prev = next; // 反转当前节点的prev指针(指向原后继)
prev = curr; // 前驱节点后移
curr = next; // 当前节点后移
}
dl->head = prev; // 反转后,原尾节点成为新头节点
printf("ReverseDouLinkList: 链表反转成功!\n");
return 0;
}
3.7 链表销毁(DestroyDouLinkList)
遍历所有节点,逐个释放内存,最后释放链表管理结构,避免内存泄漏。
int DestroyDouLinkList(DouLinkList** dl) {
if (dl == NULL || *dl == NULL) {
printf("DestroyDouLinkList: 链表已销毁或参数非法!\n");
return 1;
}
DouLinkNode* tmp = (*dl)->head;
DouLinkNode* next = NULL;
// 逐个释放节点内存
while (tmp != NULL) {
next = tmp->next;
free(tmp);
tmp = next;
}
// 释放链表管理结构
free(*dl);
*dl = NULL; // 置为NULL,避免野指针
printf("DestroyDouLinkList: 链表销毁成功!\n");
return 0;
}
3.8 辅助函数(Getsize / IsEmpty)
// 获取链表长度
int GetsizeDouLinkList(DouLinkList* dl) {
return (dl != NULL) ? dl->clen : 0;
}
// 判断链表是否为空
int IsEmptyDouLinkList(DouLinkList* dl) {
return (dl != NULL && dl->clen == 0) ? 1 : 0;
}
四、测试用例(main.c)
编写测试代码验证所有功能的正确性:
#include "doulink.h"
#include <stdio.h>
int main() {
// 1. 创建链表
DouLinkList* dl = CreateDouLinkList();
if (NULL == dl) {
return 1;
}
// 2. 定义测试数据
DATATYPE stu1 = {"张三", 18, 'M', 90};
DATATYPE stu2 = {"李四", 19, 'M', 85};
DATATYPE stu3 = {"王五", 20, 'F', 95};
DATATYPE stu4 = {"赵六", 17, 'M', 88};
DATATYPE newStu = {"张三", 18, 'M', 98}; // 修改后的张三数据
// 3. 尾部插入
InsertTailDouLinkList(dl, &stu1);
InsertTailDouLinkList(dl, &stu2);
InsertTailDouLinkList(dl, &stu3);
printf("=== 尾部插入3个节点后 ===\n");
ShowDoulinkList(dl, SHOW_FORWARD);
// 4. 头部插入
InsertHeadDouLinkList(dl, &stu4);
printf("=== 头部插入赵六后 ===\n");
ShowDoulinkList(dl, SHOW_FORWARD);
// 5. 指定位置插入(pos=2,插入到李四和王五之间)
DATATYPE stu5 = {"孙七", 19, 'F', 92};
InsertPosDouLinkList(dl, &stu5, 2);
printf("=== 位置2插入孙七后 ===\n");
ShowDoulinkList(dl, SHOW_FORWARD);
// 6. 查找节点(查找张三)
DATATYPE* findRes = FindDouLinkList(dl, "张三");
if (findRes != NULL) {
printf("=== 查找张三 ===\n");
printf("姓名:%s | 年龄:%d | 成绩:%d\n\n",
findRes->name, findRes->age, findRes->score);
}
// 7. 修改节点(修改张三的成绩为98)
ModifyDouLinkList(dl, "张三", &newStu);
printf("=== 修改张三成绩后 ===\n");
ShowDoulinkList(dl, SHOW_FORWARD);
// 8. 反转链表
ReverseDouLinkList(dl);
printf("=== 链表反转后 ===\n");
ShowDoulinkList(dl, SHOW_FORWARD);
// 9. 从尾到头遍历
printf("=== 从尾到头遍历 ===\n");
ShowDoulinkList(dl, SHOW_BACKWARD);
// 10. 删除节点(删除李四)
DeleteDoulinkList(dl, "李四");
printf("=== 删除李四后 ===\n");
ShowDoulinkList(dl, SHOW_FORWARD);
// 11. 销毁链表
DestroyDouLinkList(&dl);
ShowDoulinkList(dl, SHOW_FORWARD); // 此时链表已空
return 0;
}
五、双向链表的优点与缺点分析
5.1 优点
-
插入删除效率高:头部、尾部插入 / 删除操作时间复杂度为 O (1),无需移动大量元素(数组需移动后续元素);指定位置插入 / 删除仅需遍历到目标位置(O (n)),核心指针调整操作仍为 O (1),整体效率优于数组。
-
支持双向遍历:借助 prev 指针可直接从尾到头遍历,无需像单链表那样从头节点重新遍历,适合需要反向访问数据的场景(如日志回溯、双向迭代器)。
-
内存利用率灵活:链表节点动态分配内存,无需提前预留连续空间,可根据实际数据量动态扩展,避免数组的内存浪费或溢出问题。
-
功能扩展性强:本文实现支持自定义数据类型,可轻松扩展为存储 int、string 或复杂结构体的数据结构;同时支持查找、修改、反转等丰富操作,满足多数线性存储场景需求。
-
边界处理清晰:通过链表长度计数器(clen)简化判空、越界检查,插入 / 删除时明确处理头节点、尾节点、中间节点三种场景,逻辑严谨。
5.2 缺点
-
随机访问效率低:无法像数组那样通过索引直接访问元素,查找、指定位置插入等操作需遍历链表(时间复杂度 O (n)),数据量较大时效率下降明显。
-
额外内存开销:每个节点需额外存储 prev 和 next 两个指针,相比数组(仅存储数据)占用更多内存,数据量极大时内存开销不可忽视。
-
指针操作复杂:插入、删除、反转等操作需同时维护 prev 和 next 指针,若逻辑处理不当易出现野指针、链表断裂等问题,调试难度高于数组。
-
缓存局部性差:数组元素存储在连续内存中,CPU 缓存命中率高;而链表节点分散在堆内存中,缓存无法有效预加载,频繁访问时性能不如数组。
-
不支持快速排序:链表缺乏随机访问能力,无法高效实现基于索引的排序算法(如快速排序),排序效率通常低于数组。
六、注意事项与优化建议
6.1 注意事项
- 指针合法性:所有操作前需检查指针是否为 NULL(如链表、节点、数据),避免野指针访问导致程序崩溃。
- 内存管理:插入节点时分配内存,删除 / 销毁时必须释放内存,否则会导致内存泄漏,长期运行可能引发内存溢出。
- 边界处理:插入位置越界、删除空链表、反转节点数小于 2 等异常场景需特殊处理,避免逻辑错误。
- 字符串比较 :使用
strcmp比较字符串(不能直接用==判断地址),需确保包含<string.h>头文件。 - 线程安全 :当前实现未考虑多线程场景,若在并发环境中使用,需添加互斥锁(
pthread_mutex_t)保护链表操作,避免数据竞争。
6.2 优化建议
-
引入哨兵节点:当前实现中头节点为实际数据节点,可添加头哨兵和尾哨兵(不存储数据),简化插入 / 删除时的边界判断(无需检查头节点是否为 NULL)。
-
优化查找效率:若需频繁按关键字查找,可引入哈希表辅助存储节点指针,将查找时间复杂度优化至 O (1);或实现有序链表(插入时排序),支持二分查找(O (log n))。
-
增加排序功能:扩展按数据域排序的功能(如按成绩升序 / 降序),实现冒泡排序或归并排序(链表归并排序时间复杂度 O (n log n),空间复杂度 O (1))。
-
内存池优化 :频繁创建 / 销毁节点时,可使用内存池预分配节点内存,减少
malloc/free的开销,提升性能。 -
支持批量操作:增加批量插入、批量删除功能,减少循环中的重复判断,提升大数据量场景下的处理效率。
七、总结
本文基于 C 语言实现了一个功能完整的双向链表,涵盖初始化、插入、遍历、查找、修改、删除、反转、销毁等核心操作,通过自定义学生信息数据类型展示了链表的灵活性。双向链表的核心优势在于插入删除高效和支持双向遍历,适合数据动态变化、需频繁增删的场景(如消息队列、缓存队列、编辑器撤销栈等);但其随机访问效率低、指针操作复杂的缺点也限制了部分场景的应用。