2025/12/27 9:06:49
网站建设
项目流程
权威网站有哪些,网站为什么要进行内容更新吗,印刷网站建设价格,wordpress 获取当前文章的浏览量[C语言]双向循环链表的增删改查功能
1. 前言
本例提供一个可复用的双向循环链表模板#xff0c;含完整接口与菜单式示例主程序#xff0c;便于快速集成或学习链表操作。
2. 功能/亮点概览
双向循环 哨兵节点#xff0c;边界处理简单。增删改查全覆盖#xff0c;含头/尾…[C语言]双向循环链表的增删改查功能1. 前言本例提供一个可复用的双向循环链表模板含完整接口与菜单式示例主程序便于快速集成或学习链表操作。2. 功能/亮点概览双向循环 哨兵节点边界处理简单。增删改查全覆盖含头/尾/指定位置插入。自定义数据释放降低内存泄漏风险。UTF-8 友好Windows 终端中文不乱码。交互式菜单示例可直接运行体验。3. 核心内容解析3.1 原理与流程采用哨兵节点空表时sentinel.prev/next指向自身。插入/删除通过局部链接/断链完成时间复杂度 O(1)定位节点后。核心代码哨兵节点初始化哨兵节点的初始化是双向循环链表的基础通过将prev和next都指向自身形成一个自循环结构void list_init(list *lst) { if (lst NULL) { return; } lst-sentinel.next lst-sentinel; lst-sentinel.prev lst-sentinel; lst-sentinel.data NULL; lst-size 0; }关键点解析sentinel.next sentinel和sentinel.prev sentinel形成自循环表示空表状态sentinel.data NULL明确哨兵节点不存储数据size 0记录当前链表节点数量不含哨兵【双向循环链表结构示意图】空表时┌───────────────┐ │ sentinel │ │ prev ─────┐ │ │ next ──┐ │ │ └────┬───┴──┘ │ │ └───┘ └───────────(指向自身)含 2 个节点时┌─────────────────┐ ┌─────────────┐ ┌─────────────┐ │ sentinel │ ◀────▶ │ node1 │ ◀───▶ │ node2 │ └─────┬────┬──────┘ └─────┬───────┘ └─────┬───────┘ │ │ │ │ │ │ │ │ prev ◀────┘ └──── next prev ◀┘ next └────┐ ▲ ▲ ▲ │ │ │ │ │ └───────────┴────────────┴────────────────────────────────┘ (循环最后一个节点的 next 回到哨兵哨兵 prev回到最后节点)插入/删除实际只需设置对应指针无论头尾均不需特殊处理。每个 node 都有 prev/next两端永远指向合法节点哨兵或数据节点实现循环。⭐ 核心要点如何判断循环链表已到末尾并开始下一个循环这是双向循环链表与普通链表的根本区别在双向循环链表中没有真正的末尾因为最后一个节点的next指向哨兵哨兵的next又指向第一个节点形成闭环。遍历时通过检查当前节点是否等于哨兵节点来判断是否完成一圈。判断方法// 遍历循环链表的标准模式list_node*curlst-sentinel.next;// 从第一个数据节点开始while(cur!lst-sentinel){// 关键当 cur 等于哨兵时说明已遍历完一圈// 处理当前节点 curcurcur-next;// 移动到下一个节点}关键代码示例清空链表时的遍历void list_clear(list *lst, void (*free_data)(void *)) { if (lst NULL) { return; } list_node *cur lst-sentinel.next; // 从第一个数据节点开始 while (cur ! lst-sentinel) { // ⭐ 关键判断cur sentinel 表示已回到起点 list_node *next cur-next; // 先保存下一个节点 if (free_data ! NULL) { free_data(cur-data); } free(cur); cur next; // 移动到下一个节点 } // 循环结束后所有数据节点已释放只剩哨兵节点 lst-sentinel.next lst-sentinel; lst-sentinel.prev lst-sentinel; lst-size 0; }关键代码示例打印链表时的遍历static void print_int_list(const list *lst) { if (lst NULL || list_is_empty(lst)) { printf(列表为空。\n); return; } const list_node *cur lst-sentinel.next; // 从第一个数据节点开始 printf(列表内容: ); while (cur ! lst-sentinel) { // ⭐ 关键判断遇到哨兵说明遍历完一圈 int *val (int *)cur-data; printf(%d , val ! NULL ? *val : 0); cur cur-next; // 移动到下一个节点 } printf(\n); }关键代码示例查找节点时的遍历list_node *list_find(list *lst, int (*predicate)(void *data, void *ctx), void *ctx) { if (lst NULL || predicate NULL) { return NULL; } list_node *cur lst-sentinel.next; // 从第一个数据节点开始 while (cur ! lst-sentinel) { // ⭐ 关键判断遍历完一圈后自动停止 if (predicate(cur-data, ctx)) { return cur; // 找到匹配节点立即返回 } cur cur-next; // 继续下一个节点 } return NULL; // 遍历完一圈未找到返回 NULL }重要理解循环链表没有 NULL 指针作为结束标志普通链表用cur ! NULL判断结束哨兵节点作为循环的起点和终点cur sentinel表示已回到起点遍历完成遍历从sentinel.next开始这是第一个数据节点遍历到sentinel时停止此时已访问完所有数据节点回到起点如果继续cur cur-next会再次进入数据节点区域形成无限循环除非有额外条件控制对比普通链表// 普通链表有 NULL 结尾while(cur!NULL){// NULL 作为结束标志// 处理节点curcur-next;}// 循环链表哨兵作为结束标志while(cur!sentinel){// 哨兵作为结束标志// 处理节点curcur-next;}3.2 核心实现讲解结构体list_nodeprev/next/datalistsentinel size。接口list_create/init/destroy/clearlist_push_front/back/insert_afterlist_removelist_get_atlist_findlist_replace_datalist_is_emptylist_length。核心代码数据结构定义typedef struct list_node { struct list_node *prev; struct list_node *next; void *data; } list_node; typedef struct list { list_node sentinel; // 哨兵节点prev/next 指向自身表示空表 size_t size; } list;关键点解析list_node包含双向指针prev/next和通用数据指针datavoid *支持任意类型list结构包含一个哨兵节点非指针和大小计数器哨兵作为链表的一部分核心代码节点链接函数插入操作的核心是link_between函数它负责在prev和next之间插入新节点static void link_between(list_node *prev, list_node *next, list_node *node) { // prev - node - next node-prev prev; node-next next; prev-next node; next-prev node; }关键点解析四步操作设置新节点的prev/next然后更新相邻节点的指针无论插入位置头/尾/中间逻辑统一无需特殊处理核心代码头插和尾插实现list_node *list_push_front(list *lst, void *data) { if (lst NULL) { return NULL; } list_node *node alloc_node(data); if (node NULL) { return NULL; } link_between(lst-sentinel, lst-sentinel.next, node); lst-size; return node; } list_node *list_push_back(list *lst, void *data) { if (lst NULL) { return NULL; } list_node *node alloc_node(data); if (node NULL) { return NULL; } link_between(lst-sentinel.prev, lst-sentinel, node); lst-size; return node; }关键点解析头插在sentinel和sentinel.next之间插入尾插在sentinel.prev和sentinel之间插入空表时sentinel.next sentinel.prev sentinel插入逻辑依然正确核心代码节点删除实现static void unlink_node(list_node *node) { node-prev-next node-next; node-next-prev node-prev; node-prev NULL; node-next NULL; }int list_remove(list *lst, list_node *node, void (*free_data)(void *)) { if (lst NULL || node NULL || node lst-sentinel) { return 0; } unlink_node(node); if (free_data ! NULL) { free_data(node-data); } free(node); lst-size--; return 1; }关键点解析unlink_node先更新相邻节点指针再清空被删节点的指针防止误用list_remove检查不能删除哨兵节点支持自定义数据释放回调3.3 输入/输出与验证示例main使用scanf读取整数下标从 0 开始。删除/修改/查询前校验下标有效性。核心代码输入验证函数static int read_int(const char *prompt, int *out) { if (prompt ! NULL) { printf(%s, prompt); } if (scanf(%d, out) ! 1) { // 清理残留输入 int ch; while ((ch getchar()) ! \n ch ! EOF) { } return 0; } return 1; }关键点解析scanf返回值检查成功读取返回 1失败返回 0失败时清理输入缓冲区避免残留字符影响后续输入核心代码按下标访问与边界检查list_node *list_get_at(list *lst, size_t index) { if (lst NULL || index lst-size) { return NULL; } list_node *cur lst-sentinel.next; size_t i 0; while (cur ! lst-sentinel i index) { cur cur-next; i; } return (cur lst-sentinel) ? NULL : cur; }关键点解析先检查index size避免无效访问从sentinel.next开始遍历循环条件同时检查是否到达哨兵和索引位置返回NULL表示下标越界或链表为空核心代码删除操作中的验证示例case 2: { // 删除 if (list_is_empty(lst)) { printf(列表为空无可删除项。\n); break; } int idx; if (!read_int(请输入要删除的下标(0 基): , idx) || idx 0) { printf(输入无效操作取消。\n); break; } list_node *node list_get_at(lst, (size_t)idx); if (node NULL) { printf(未找到该下标的节点。\n); break; } if (list_remove(lst, node, free)) { printf(删除成功。\n); } else { printf(删除失败。\n); } break; }关键点解析先检查链表是否为空验证输入是否为有效整数且非负通过list_get_at获取节点返回NULL表示下标无效所有验证通过后才执行删除操作3.4 优化与注意事项使用自定义free_data释放存储数据避免泄漏。UTF-8 控制台设置Windows防止中文提示乱码。单线程示例如需多线程需自行加锁。核心代码内存管理 - 清空链表void list_clear(list *lst, void (*free_data)(void *)) { if (lst NULL) { return; } list_node *cur lst-sentinel.next; while (cur ! lst-sentinel) { list_node *next cur-next; if (free_data ! NULL) { free_data(cur-data); } free(cur); cur next; } lst-sentinel.next lst-sentinel; lst-sentinel.prev lst-sentinel; lst-size 0; }关键点解析先保存next指针再释放节点避免访问已释放内存支持自定义free_data回调灵活处理不同类型的数据释放清空后重置哨兵指针和大小恢复空表状态核心代码UTF-8 控制台设置// 设置控制台为 UTF-8避免中文输出乱码主要针对 Windows 控制台 static void set_console_utf(void) { #ifdef _WIN32 SetConsoleOutputCP(65001); SetConsoleCP(65001); #endif }关键点解析代码页 65001 对应 UTF-8 编码使用条件编译#ifdef _WIN32仅在 Windows 平台生效Linux/Mac 默认使用 UTF-8无需额外设置核心代码程序退出时的资源清理list_destroy(lst, free); return 0;关键点解析list_destroy内部调用list_clear清理所有节点传入标准库的free函数释放整型数据示例中每个整数单独分配最后释放链表结构本身确保无内存泄漏4. 关键技术点哨兵循环链表的边界简化。可插拔的内存释放回调。菜单驱动主函数集中演示 CRUD。5. 复杂度/成本分析查找/按下标访问O(n)线性遍历。插入/删除O(1)已定位节点后。额外空间节点 O(1) 每元素哨兵常数开销。6. 运行示例在link-list目录gcc list.c -o list_demo ./list_demo# Windows 运行 list_demo.exe菜单项1 添加、2 删除、3 修改、4 查询、5 打印、0 退出。7. 完整代码/资源7.1 list.h头文件// 双向循环链表接口#ifndefLIST_H#defineLIST_H#includestddef.htypedefstructlist_node{structlist_node*prev;structlist_node*next;void*data;}list_node;typedefstructlist{list_node sentinel;// 哨兵节点prev/next 指向自身表示空表size_tsize;}list;// 创建链表返回堆上分配的链表指针失败返回 NULLlist*list_create(void);// 初始化已分配的链表结构使用栈或静态分配时调用voidlist_init(list*lst);// 清空链表节点free_data 用于释放 data允许传入 NULL 表示不处理 datavoidlist_clear(list*lst,void(*free_data)(void*));// 销毁链表并释放节点包含 data 的自定义释放voidlist_destroy(list*lst,void(*free_data)(void*));// 头插返回新节点指针失败返回 NULLlist_node*list_push_front(list*lst,void*data);// 尾插返回新节点指针失败返回 NULLlist_node*list_push_back(list*lst,void*data);// 在指定节点之后插入pos 允许为 sentinel 表示头部前插返回新节点或 NULLlist_node*list_insert_after(list*lst,list_node*pos,void*data);// 移除指定节点free_data 用于释放 data可为 NULL成功返回 1失败返回 0intlist_remove(list*lst,list_node*node,void(*free_data)(void*));// 判断是否为空intlist_is_empty(constlist*lst);// 获取节点数量size_tlist_length(constlist*lst);// 按下标获取节点0 基失败返回 NULLlist_node*list_get_at(list*lst,size_tindex);// 查找符合条件的首个节点predicate 返回非 0 表示匹配未找到返回 NULLlist_node*list_find(list*lst,int(*predicate)(void*data,void*ctx),void*ctx);// 替换指定节点的数据free_old 用于释放旧 data可为 NULL成功返回 1失败返回 0intlist_replace_data(list*lst,list_node*node,void*new_data,void(*free_old)(void*));#endif// LIST_H7.2 list.c实现文件// 双向循环链表实现#includelist.h#includestdlib.h#includestdio.h#ifdef_WIN32#includewindows.h#endifstaticlist_node*alloc_node(void*data){list_node*node(list_node*)malloc(sizeof(list_node));if(nodeNULL){returnNULL;}node-datadata;node-prevNULL;node-nextNULL;returnnode;}staticvoidlink_between(list_node*prev,list_node*next,list_node*node){// prev - node - nextnode-prevprev;node-nextnext;prev-nextnode;next-prevnode;}list*list_create(void){list*lst(list*)malloc(sizeof(list));if(lstNULL){returnNULL;}list_init(lst);returnlst;}voidlist_init(list*lst){if(lstNULL){return;}lst-sentinel.nextlst-sentinel;lst-sentinel.prevlst-sentinel;lst-sentinel.dataNULL;lst-size0;}staticvoidunlink_node(list_node*node){node-prev-nextnode-next;node-next-prevnode-prev;node-prevNULL;node-nextNULL;}voidlist_clear(list*lst,void(*free_data)(void*)){if(lstNULL){return;}list_node*curlst-sentinel.next;while(cur!lst-sentinel){list_node*nextcur-next;if(free_data!NULL){free_data(cur-data);}free(cur);curnext;}lst-sentinel.nextlst-sentinel;lst-sentinel.prevlst-sentinel;lst-size0;}voidlist_destroy(list*lst,void(*free_data)(void*)){if(lstNULL){return;}list_clear(lst,free_data);free(lst);}list_node*list_push_front(list*lst,void*data){if(lstNULL){returnNULL;}list_node*nodealloc_node(data);if(nodeNULL){returnNULL;}link_between(lst-sentinel,lst-sentinel.next,node);lst-size;returnnode;}list_node*list_push_back(list*lst,void*data){if(lstNULL){returnNULL;}list_node*nodealloc_node(data);if(nodeNULL){returnNULL;}link_between(lst-sentinel.prev,lst-sentinel,node);lst-size;returnnode;}list_node*list_insert_after(list*lst,list_node*pos,void*data){if(lstNULL||posNULL){returnNULL;}// 允许 pos 为 sentinel实现头部插入等场景list_node*nodealloc_node(data);if(nodeNULL){returnNULL;}link_between(pos,pos-next,node);lst-size;returnnode;}intlist_remove(list*lst,list_node*node,void(*free_data)(void*)){if(lstNULL||nodeNULL||nodelst-sentinel){return0;}unlink_node(node);if(free_data!NULL){free_data(node-data);}free(node);lst-size--;return1;}intlist_is_empty(constlist*lst){returnlstNULL||lst-size0;}size_tlist_length(constlist*lst){returnlstNULL?0:lst-size;}list_node*list_get_at(list*lst,size_tindex){if(lstNULL||indexlst-size){returnNULL;}list_node*curlst-sentinel.next;size_ti0;while(cur!lst-sentineliindex){curcur-next;i;}return(curlst-sentinel)?NULL:cur;}list_node*list_find(list*lst,int(*predicate)(void*data,void*ctx),void*ctx){if(lstNULL||predicateNULL){returnNULL;}list_node*curlst-sentinel.next;while(cur!lst-sentinel){if(predicate(cur-data,ctx)){returncur;}curcur-next;}returnNULL;}intlist_replace_data(list*lst,list_node*node,void*new_data,void(*free_old)(void*)){if(lstNULL||nodeNULL||nodelst-sentinel){return0;}if(free_old!NULL){free_old(node-data);}node-datanew_data;return1;}// 设置控制台为 UTF-8避免中文输出乱码主要针对 Windows 控制台staticvoidset_console_utf(void){#ifdef_WIN32SetConsoleOutputCP(65001);SetConsoleCP(65001);#endif}// 以下为示例主函数便于使用 GCC 直接编译运行演示// gcc list.c -o list_demo// 程序演示读取用户输入的数据集将其存入双向循环链表并打印staticvoidprint_int_list(constlist*lst){if(lstNULL||list_is_empty(lst)){printf(列表为空。\n);return;}constlist_node*curlst-sentinel.next;printf(列表内容: );while(cur!lst-sentinel){int*val(int*)cur-data;printf(%d ,val!NULL?*val:0);curcur-next;}printf(\n);}staticintread_int(constchar*prompt,int*out){if(prompt!NULL){printf(%s,prompt);}if(scanf(%d,out)!1){// 清理残留输入intch;while((chgetchar())!\nch!EOF){}return0;}return1;}staticint*alloc_int_value(intv){int*p(int*)malloc(sizeof(int));if(p!NULL){*pv;}returnp;}staticvoidprint_menu(void){printf(\n 双向循环链表演示 \n);printf(1. 添加尾插\n);printf(2. 删除按下标\n);printf(3. 修改按下标\n);printf(4. 查询按下标\n);printf(5. 打印全部\n);printf(0. 退出\n);}intmain(void){set_console_utf();list*lstlist_create();if(lstNULL){fprintf(stderr,创建链表失败。\n);return1;}intrunning1;while(running){print_menu();intchoice-1;if(!read_int(请选择操作: ,choice)){printf(输入无效请重试。\n);continue;}switch(choice){case1:{// 添加intvalue;if(!read_int(请输入要添加的整数: ,value)){printf(输入无效操作取消。\n);break;}int*palloc_int_value(value);if(pNULL){printf(内存分配失败添加中止。\n);break;}if(list_push_back(lst,p)NULL){printf(插入节点失败添加中止。\n);free(p);break;}printf(添加成功。\n);break;}case2:{// 删除if(list_is_empty(lst)){printf(列表为空无可删除项。\n);break;}intidx;if(!read_int(请输入要删除的下标(0 基): ,idx)||idx0){printf(输入无效操作取消。\n);break;}list_node*nodelist_get_at(lst,(size_t)idx);if(nodeNULL){printf(未找到该下标的节点。\n);break;}if(list_remove(lst,node,free)){printf(删除成功。\n);}else{printf(删除失败。\n);}break;}case3:{// 修改if(list_is_empty(lst)){printf(列表为空无可修改项。\n);break;}intidx;if(!read_int(请输入要修改的下标(0 基): ,idx)||idx0){printf(输入无效操作取消。\n);break;}list_node*nodelist_get_at(lst,(size_t)idx);if(nodeNULL){printf(未找到该下标的节点。\n);break;}intnew_val;if(!read_int(请输入新的整数值: ,new_val)){printf(输入无效操作取消。\n);break;}int*palloc_int_value(new_val);if(pNULL){printf(内存分配失败修改中止。\n);break;}if(list_replace_data(lst,node,p,free)){printf(修改成功。\n);}else{printf(修改失败。\n);free(p);}break;}case4:{// 查询if(list_is_empty(lst)){printf(列表为空。\n);break;}intidx;if(!read_int(请输入要查询的下标(0 基): ,idx)||idx0){printf(输入无效操作取消。\n);break;}list_node*nodelist_get_at(lst,(size_t)idx);if(nodeNULL){printf(未找到该下标的节点。\n);break;}int*val(int*)node-data;printf(下标 %d 的值为: %d\n,idx,val!NULL?*val:0);break;}case5:{// 打印全部print_int_list(lst);break;}case0:running0;break;default:printf(无效选项请重试。\n);break;}}list_destroy(lst,free);return0;}7.3 代码仓库完整代码已上传至 GitHub/Gitee可直接下载使用。项目地址[项目链接]编译命令gcc list.c -o list_demo运行./list_demo# Linux/Maclist_demo.exe# Windows8. 总结该实现以哨兵双向循环链表为基础提供完整 CRUD 接口与可运行示例适合作为学习模板或嵌入小型项目。根据业务可替换数据类型与释放回调并在多线程场景加锁。9. 三连提示如果对你有帮助欢迎点赞、收藏、关注。***