嵌入式 数据结构学习(四) 双向链表详解与工程管理
一、双向链表基础
双向链表示意图(指针永远指向内存开始的区域)
二、双向链表核心操作
1. 创建双向链表
c
/*** 创建双向链表头节点* @return 成功返回链表指针,失败返回NULL*/ struct DouLinkList* CreateDouLinkList() {// 分配链表头结构体内存struct DouLinkList* dl = (struct DouLinkList*)malloc(sizeof(struct DouLinkList));if(NULL == dl){fprintf(stderr, "CreateDouLinkList malloc error\n");return NULL;}// 初始化链表为空状态dl->head = NULL; // 头指针置空dl->clen = 0; // 长度计数器归零return dl; }
2. 头插法插入节点
图解第一种情况:
图解第二种情况:
c
/*** 在链表头部插入新节点* @param dl 链表指针* @param data 要插入的数据指针* @return 成功返回0,失败返回1*/ int InsertHeadDouLinkList(struct DouLinkList* dl, struct DATATYPE* data) {// 为新节点分配内存struct DouNode* newnode = (struct DouNode*)malloc(sizeof(struct DouNode));if(NULL == newnode){fprintf(stderr, "InsertHeadDouLinkList malloc failed\n");return 1;}// 拷贝数据到新节点memcpy(&newnode->data, data, sizeof(struct DATATYPE));// 初始化新节点指针newnode->next = NULL;newnode->prev = NULL;// 新节点指向原头节点newnode->next = dl->head;// 如果原链表非空,设置原头节点的前驱指针if(dl->head){dl->head->prev = newnode;}// 更新链表头指针dl->head = newnode;// 链表长度增加dl->clen++;return 0; }
3. 双向遍历链表
c
/*** 遍历打印链表内容* @param dl 链表指针* @param dir 遍历方向(FORWADR正向/BACKWADR反向)* @return 总是返回0*/ int ShowDouLinkList(struct DouLinkList* dl, DIR dir) {struct DouNode* tmp = dl->head;if(FORWADR == dir) // 正向遍历{printf("Forward traversal:\n");while(tmp){// 打印节点数据printf("Name: %s, Sex: %c, Age: %d, Score: %d\n", tmp->data.name, tmp->data.sex, tmp->data.age, tmp->data.score);tmp = tmp->next; // 移动到下一个节点}}else if(BACKWADR == dir) // 反向遍历{printf("Backward traversal:\n");// 先移动到链表尾部while(tmp->next){tmp = tmp->next;}// 从尾部向前遍历while(tmp){printf("Name: %s, Sex: %c, Age: %d, Score: %d\n", tmp->data.name, tmp->data.sex, tmp->data.age, tmp->data.score);tmp = tmp->prev; // 移动到前一个节点}}return 0; }
判断双链表是否为空
int IsEmptyDouLinkList(struct DouLinkList* dl)
{
return 0 == dl->clen; // 若链表长度(clen)为0,返回1(真),否则返回0(假)
}获取双链表长度
int GetSizeDouLinkList(struct DouLinkList* dl)
{
return dl->clen; // 直接返回链表长度(clen)
}
4. 尾插法插入节点
c
/*** 在链表尾部插入新节点* @param dl 链表指针* @param data 要插入的数据指针* @return 成功返回0,失败返回1*/ int InserTailDouLinkList(struct DouLinkList* dl, struct DATATYPE* data) {// 空链表直接调用头插法if(IsEmptyDouLinkList(dl)){return InsertHeadDouLinkList(dl, data);}// 查找尾节点struct DouNode* tmp = dl->head;while(tmp->next){tmp = tmp->next;}// 创建新节点struct DouNode* newnode = malloc(sizeof(struct DouNode));if(newnode == NULL){fprintf(stderr, "InserTailDouLinkList malloc failed\n");return 1;}// 初始化新节点memcpy(&newnode->data, data, sizeof(struct DATATYPE));newnode->next = NULL; // 尾节点的next为NULLnewnode->prev = tmp; // 前驱指向原尾节点// 将新节点链接到链表尾部tmp->next = newnode;// 链表长度增加dl->clen++;return 0; }
5. 指定位置插入节点
c
/*** 在指定位置插入新节点* @param dl 链表指针* @param data 要插入的数据指针* @param pos 插入位置(0-based)* @return 成功返回0,失败返回1*/ int InserPosDouLinkList(struct DouLinkList* dl, struct DATATYPE* data, int pos) {int len = GetSizeDouLinkList(dl);// 检查位置合法性if(pos < 0 || pos > len){fprintf(stderr, "Invalid position %d\n", pos);return 1;}// 处理头插和尾插特殊情况if(0 == pos){return InsertHeadDouLinkList(dl, data);}else if(pos == len){return InserTailDouLinkList(dl, data);}// 创建新节点struct DouNode* newnode = malloc(sizeof(struct DouNode));if(newnode == NULL){fprintf(stderr, "InserPosDouLinkList malloc failed\n");return 1;}// 初始化新节点数据memcpy(&newnode->data, data, sizeof(struct DATATYPE));newnode->next = NULL;newnode->prev = NULL;// 定位到插入位置的前一个节点struct DouNode* tmp = dl->head;for(int i = 0; i < pos - 1; ++i){tmp = tmp->next;}// 重新链接指针newnode->next = tmp->next; // 新节点后继指向原位置节点newnode->prev = tmp; // 新节点前驱指向前驱节点tmp->next->prev = newnode; // 原位置节点的前驱指向新节点tmp->next = newnode; // 前驱节点的后继指向新节点// 链表长度增加dl->clen++;return 0; }
6. 查找节点
c
/*** 按姓名查找节点* @param dl 链表指针* @param name 要查找的姓名* @return 找到返回节点指针,未找到返回NULL*/ struct DouNode* FindDouLinkList(struct DouLinkList* dl, char *name) {// 空链表直接返回NULLif(IsEmptyDouLinkList(dl)){return NULL;}struct DouNode* tmp = dl->head;// 遍历链表查找匹配节点while(tmp){if(strcmp(tmp->data.name, name) == 0){return tmp; // 找到匹配节点}tmp = tmp->next; // 继续检查下一个节点}return NULL; // 遍历结束未找到 }
7. 修改节点数据
c
/*** 修改指定节点的数据* @param dl 链表指针* @param name 要修改的节点姓名* @param data 新数据指针* @return 成功返回0,失败返回1*/ int ModifyDouLinkList(struct DouLinkList* dl, char *name, struct DATATYPE* data) {// 查找目标节点struct DouNode* ret = FindDouLinkList(dl, name);if(NULL == ret){fprintf(stderr, "Node %s not found\n", name);return 1;}// 拷贝新数据到目标节点memcpy(&ret->data, data, sizeof(struct DATATYPE));return 0; }
8. 双向链表逆序
c
/*** 反转双向链表* @param dl 链表指针* @return 成功返回0,失败返回1*/ int RevertDouLinkList(struct DouLinkList* dl) {int len = GetSizeDouLinkList(dl);// 长度小于2无需反转if(len < 2){return 1;}// 初始化三个工作指针struct DouNode* Prev = NULL; // 前驱指针struct DouNode* Tmp = dl->head; // 当前指针struct DouNode* Next = Tmp->next; // 后继指针while(1){// 反转当前节点的指针Tmp->prev = Next; // 原后继变为前驱Tmp->next = Prev; // 原前驱变为后继// 移动指针Prev = Tmp; // 前驱指针前进Tmp = Next; // 当前指针前进// 检查是否到达链表末尾if(NULL == Tmp){break;}// 更新后继指针Next = Next->next;}// 更新链表头指针dl->head = Prev;return 0; }
9. 删除指定节点
c
/*** 删除指定节点* @param dl 链表指针* @param name 要删除的节点姓名* @return 成功返回0,失败返回1*/ int DeleteDouLinkList(struct DouLinkList* dl, char* name) {// 查找目标节点struct DouNode* tmp = FindDouLinkList(dl, name);if(NULL == tmp){fprintf(stderr, "Node %s not found\n", name);return 1;}// 处理头节点情况if(tmp == dl->head){dl->head = dl->head->next;if(dl->head){dl->head->prev = NULL; // 新头节点的前驱置空}}// 处理尾节点情况else if(NULL == tmp->next){if(tmp->prev){tmp->prev->next = NULL; // 前驱节点的后继置空}else{dl->head = NULL; // 链表只有一个节点的情况}}// 处理中间节点情况else{// 更新后继节点的前驱指针if(tmp->next) {tmp->next->prev = tmp->prev;}// 更新前驱节点的后继指针if(tmp->prev) {tmp->prev->next = tmp->next;}}// 释放节点内存free(tmp);// 链表长度减少dl->clen--;return 0; }
10. 销毁链表
c
/*** 销毁整个链表* @param dl 链表指针* @return 总是返回0*/ int DestroyDouLinkList(struct DouLinkList* dl) {struct DouNode* tmp = dl->head;// 循环释放所有节点while(tmp){dl->head = dl->head->next; // 头指针后移free(tmp); // 释放当前节点tmp = dl->head; // 指向新的头节点}// 释放链表头结构free(dl);return 0; }
三、Makefile工程管理
基础版本
makefile
# 简单Makefile示例 # 目标:依赖 a.out: main.c ./doulink.c# 编译命令(前面必须是tab)gcc main.c doulink.c# 清理目标 clean:rm a.out
推荐版本
makefile
# 定义编译器和编译选项 CC = gcc CFLAGS = -Wall -g # 开启所有警告和调试信息# 定义目标可执行文件 TARGET = app# 定义源文件 SRCS = main.c doulink.c# 默认目标规则 $(TARGET): $(SRCS)$(CC) $(CFLAGS) -o $@ $^# 清理规则 clean:rm $(TARGET).PHONY: clean # 声明clean为伪目标
使用说明:
-
编译:
make
(自动执行第一条规则) -
运行:
./app
(或./a.out如果是基础版本) -
清理:
make clean
(删除生成的可执行文件)
四、双向链表VS单向链表对比
特性 | 单向链表 | 双向链表 |
---|---|---|
遍历方向 | 仅能正向遍历 | 可双向遍历 |
节点结构 | data + next | data + prev + next |
插入删除 | 删除需遍历找前驱(O(n)) | 直接操作前驱节点(O(1)) |
内存占用 | 较小(少一个指针) | 较大(多一个指针) |
适用场景 | 简单线性数据,内存紧张 | 需要频繁查找前驱或反向遍历 |
五、嵌入式开发建议
-
资源受限系统:
-
优先考虑单向链表节省内存
-
静态分配节点内存池避免碎片
-
-
实时性要求高:
-
双向链表删除操作更高效
-
可考虑使用循环双向链表
-
-
调试技巧:
bash
gdb ./your_program (gdb) b DouLinkList.c:100 # 在指定行设断点 (gdb) p *node # 查看节点内容 (gdb) bt # 查看调用栈
-
性能优化:
-
维护尾指针加速尾插操作
-
使用内存池预分配节点
-