当前位置: 首页 > news >正文

还在被双链表绕晕?这篇保姆级教程带你彻底吃透(含完整C实现)

月华透云上枝头,稚雏也学雀南飞。

此朝花间挑芙蓉,莺莺燕燕娇欲滴。

岂敢良景乏乏眼,正是华灯初上时。

可将此生留其中,往是你去也是你。

1. 链表的分类

1.1单双向

单向链表(以单链表为例子)双向链表(以双链表为例)

1.2带头或不带头

不带头:头节点直接存储有效数据

带头:头节点不存储有效数据,而是存储第一个有效节点的地址(可视为占位子或者哨兵位)

1.3循环和不循环

不循环(存在尾结点,也可以说某个节点的next指针指向NULL)

循环(所有结点中的next指针没有指向NULL)

综上所述可以列举出八种不同的链表类型,日常主要是应用两种链表:

Ⅰ:无头单向非循环链表(即单链表)

结构简单,一般不会用于单独存储数据,实际应用于作为其他数据结构的子结构,如哈希桶,图的邻接表等

Ⅱ:带头双向循环链表

结构复杂,一般用于单独存储数据,一个节点由三个部分组成:前驱指针+后驱指针+保存数据

2.双链表的接头实现(C语言)

2.1对双链表类型的成员定义(包含的重命名)

typedef int LTDataType; typedef struct ListNode { LTDataType data;//要存储的有效数据 struct ListNode* next;//保存下一个节点地址的指针 struct ListNode* prev;//保存上一个节点地址的指针 }LTNode;

2.2双链表初始化

以下代码存在一些问题,pphead可能不为空指针,但*pphead可能为空指针,因为情况一:此时链表中不存在节点,此处初始化的节点是第一个节点运行程序时可能会出现问题

void LTInit(LTNode** pphead) { assert(pphead);//判断是否为NULL,防止对空指针解引用 (*pphead)->data = -1;//将存储的有效数据初始化为-1 (*pphead)->prev = (*pphead)->next = *pphead;//前驱指针和后驱指针全部指向自身 }

2.0初始化版本

void LTInit(LTNode** pphead) { assert(pphead); LTBuyNode(-1);//后面的函数调用,作用是申请一个节点的空间,并将有效数据保存其中 }

2.3双链表创建节点空间

LTNode* LTBuyNode(LTDataType x) { LTNode* newnode = (LTNode*)malloc(sizeof(LTNode)); //申请一个节点的空间,并且保存这个空间的地址给newnode if (newnode == NULL) { perror("malloc fail!"); exit(1); }//判断是否申请空间成功 newnode->data = x;//将数据保存到新申请的节点中 newnode->prev = newnode->next = newnode;//将前驱指针和后驱指针全部指向自身 return newnode;//返回新节点的地址 }

2.4双链表的打印

void LTPrint(LTNode* phead) { LTNode* pcur = phead->next; while (pcur != phead) { printf("%d->", pcur->data); pcur = pcur->next; } printf("\n"); }

2.5双链表的尾插(头节点不需要改变则传一级指针,否则传二级指针)

观察上方两幅图示可知仅需改变:

原链表:d3的next指向newnode

head的prev指向newnode

新节点:prev指向d3

next指向head

void LTPushBack(LTNode* phead, LTDataType x) { assert(phead); LTNode* newnode = LTBuyNode(x); //优先改变对原链表无影响的部分,即新插入的节点 newnode->prev = phead->prev; newnode->next = phead; //再修改原链表 phead->prev->next = newnode;//尾节点修改 phead->prev = newnode;//头节点修改 }

2.6判断链表是否为空(仅有一个头节点的情况下双链表为空)

bool LTEmpty(LTNode* phead) { assert(phead); return phead->next == phead; }

2.7双链表的尾删(注意:链表不可为空)

主要修改对象是:头节点和倒数第二个节点

修改对象:倒数第二个节点的next指向头节点

head->prev指向倒数第二个节点

void LTPopBack(LTNode* phead) { assert(!LTEmpty(phead)); LTNode* del = phead->prev; del->prev->next = phead; phead->prev = del->prev; free(del); del = NULL; }

2.8双链表的头插(在第一个有效节点前插入)

注意:不能先使head->next指向new->prev,这样会使原链表的第一个节点地址丢失

故先使d1->prev指向newnode,再使head->next指向newnode

void LTPushFront(LTNode* phead, LTDataType x) { assert(phead); LTNode* newnode = LTBuyNode(x); newnode->next = phead->next;//新节点的next指向原链表的第一个节点 newnode->prev = phead;//新节点的prev指向头节点 phead->next->prev = newnode;//原链表的第一个节点的prev指向新节点 phead->next = newnode;//头节点的next指向新节点 }

2.9双链表的头删

修改对象:d2的prev指向head

head的next指向d2

void LTPopFront(LTNode* phead) { assert(!LTEmpty(phead)); LTNode* del = phead->next;//del指向第一个有效节点 del->next->prev = phead;//原链表的第二个节点的prev指向头节点 phead->next = del->next;//头节点的next指向原链表的第二个节点 free(del); del = NULL; }

2.10双链表的查找

LTNode* LTFind(LTNode* phead, LTDataType x) { assert(phead); LTNode* pcur = phead->next;//pcur初始化指向第一个有效节点 while (pcur != phead) { if (pcur->data == x) { return pcur; } pcur = pcur->next;//pcur指针不断向后移动,直到指向头节点退出 } return NULL; }

2.11删除双链表中指定位置pos的节点

void LTErase(LTNode* pos) { assert(pos); pos->next->prev = pos->prev;//pos的下一个节点的prev指向pos的前一个节点 pos->prev->next = pos->next;//pos的前一个节点的next指向pos的下一个节点 free(pos); pos = NULL; }

2.12在指定位置pos之后插入数据

处理对象:Ⅰ:先对原链表无影响的newnode进行处理

newnode的next指向pos的next

newnode的prev指向pos

Ⅱ:不可以通过原链表进行改动,读者大可一试,小编try过几次,还是觉得通过新节点间接改动原链表更有效且正确

void LTInsert(LTNode* pos, LTDataType x) { assert(pos); LTNode* newnode = LTBuyNode(x); newnode->next = pos->next; newnode->prev = pos; pos->next->prev = newnode; pos->next = newnode; }

2.13销毁链表

void LTDestroy(LTNode** pphead) { LTNode* pcur = (*pphead)->next; while (pcur != *pphead) { LTNode* next = pcur->next; free(pcur); pcur = next; } free(*pphead); *pphead = NULL; }

双链表的销毁需要一个一个节点单独销毁,小编给出的销毁实现函数是传入二级指针,有违背接口的一致性,各位读者也可以改为一级指针,之后在调用完函数之后手动置空

http://www.cnnetsun.cn/news/2520370.html

相关文章:

  • 衔接器CC Switch 小白图文安装,接入Claude Opus4.7+deekseep V4 +千问等等都不在话下,再也不用担心无法配置几个第三方大模型。
  • 【YOLO目标检测全栈实战】65 让YOLO开口说话:YOLO-World + 多模态大模型的端到端对话系统实战
  • 逆向工程学习日志(第五天):常见加密算法特征识别与 Python 打包程序的逆向边界
  • CANN模型编译与离线部署全攻略
  • 海克斯大乱斗:普攻英雄“锻体”收益的严谨数学分析
  • AI安全新范式:用逆向推理与因果推断定位系统性风险
  • 面试:如果让你设计一个客服 Agent,你会如何划分四大组件的职责?
  • D盾深度集成IIS:Windows Web服务器原生级Webshell防护方案
  • Frida Hook SSL_read/SSL_write 实现HTTPS明文流量捕获
  • Agentic o3调度器与Gemma/Nemotron-H推理范式演进
  • Unity跨平台发布失败的根因分析与七步排查法
  • Hugging Face实战备忘录:开发者必备的AI开发OS层指南
  • AI-native开发:从工具使用者到智能体编排工程师的范式跃迁
  • 医疗数据中心AI:面向临床确定性的边缘智能架构
  • TensorFlow Federated核心原理:联邦计算契约与类型系统解析
  • 房地产数字沙盘价格与服务商选型指南,2026年开发商采购参考
  • GPT-4的1.8万亿参数与2%激活:MoE稀疏推理实战解析
  • 服务器GPU直通故障根因与五层协同调试指南
  • GitLab CVE-2025-1477:URI编码绕过身份验证的应急防护指南
  • 深度学习学习率调度器原理与工业级实战指南
  • AI资讯简报如何成为工程师的技术决策雷达
  • 把AI的能力拆成乐高积木:如何让Agent真正干成复杂的事
  • 开源Agent框架能跑通Demo,但离企业生产还差五个能力
  • 真实系统弱口令爆破的三大硬核细节:Payload位置、滑动窗口与请求指纹
  • Phi-3.5与Minitron小模型技术路径深度对比
  • 滤光片原理与应用:从光谱管理到光学系统性能提升
  • TensorFlow手写单词识别:CNN-LSTM-CTC实战指南
  • 从零搭建 AI 搜索引擎:我给装上了智能记忆,还踩了这些坑
  • 三方物流城市配送仓运配一体化解决方案(基于JeeWMS·模块化可拆分部署版)
  • AI信息筛选操作系统:从过载到可验证的工程实践