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

双向链表与通用型容器

一、双向链表基本概念

双向链表是一种常见的数据结构,每个节点包含两个指针:一个指向前一个节点(prev),一个指向后一个节点(next)。与单向链表相比,双向链表支持双向遍历,操作更为灵活,尤其适合需要频繁前后遍历的场景。

核心特点

  • 每个节点包含数据域和两个指针域
  • 支持从头到尾或从尾到头的双向遍历
  • 实际应用中常采用循环结构(首尾相连)

二、双向链表设计与实现

1. 节点设计

typedef struct node
{int data;               // 数据域struct node* prev_p;    // 前驱指针struct node* next_p;    // 后继指针
} node_t, *node_p;

2. 初始化空链表(头节点)

node_p DLINK_LIST_InitHeadNode(void)
{node_p p = malloc(sizeof(node_t));if (p != NULL) {p->prev_p = NULL;p->next_p = NULL;return p;}return NULL;
}

关键点

  • 头节点不存储实际数据
  • 初始时两个指针均指向NULL
  • 头节点用于简化链表操作

3. 初始化数据节点

node_p DLINK_LIST_InitDataNode(int data)
{node_p p = malloc(sizeof(node_t));if (p != NULL) {p->data = data;p->prev_p = NULL;p->next_p = NULL;return p;}return NULL;
}

4. 判断链表是否为空

bool DLINK_LIST_IfEmpty(node_p head_node)
{return (head_node->prev_p == NULL) && (head_node->next_p == NULL);
}

5. 插入操作

头插法

void DLINK_LIST_HeadInsertDataNode(node_p head_node, node_p new_node)
{if (DLINK_LIST_IfEmpty(head_node)) {new_node->prev_p = head_node;new_node->next_p = NULL;head_node->next_p = new_node;} else {new_node->prev_p = head_node;new_node->next_p = head_node->next_p;head_node->next_p->prev_p = new_node;head_node->next_p = new_node;}
}

尾插法

void DLINK_LIST_TailInsertDataNode(node_p head_node, node_p new_node)
{node_p tmp_p = head_node;while (tmp_p->next_p != NULL) {tmp_p = tmp_p->next_p;}new_node->prev_p = tmp_p;new_node->next_p = NULL;tmp_p->next_p = new_node;
}

6. 遍历链表

int DLINK_LIST_ShowListData(node_p head_node)
{if (DLINK_LIST_IfEmpty(head_node))return -1;node_p tmp_p = head_node->next_p;int i = 0;printf("======================链表数据======================\n");while (tmp_p != NULL) {printf("节点%d: 数据 = %d\n", i, tmp_p->data);tmp_p = tmp_p->next_p;i++;}printf("====================================================\n");return 0;
}

7. 删除操作

int DLINK_LIST_DelDataNode(node_p head_node, int data)
{if (DLINK_LIST_IfEmpty(head_node))return -1;node_p tmp_p = head_node;while (tmp_p->next_p != NULL) {if (tmp_p->next_p->data == data) {node_p del_node = tmp_p->next_p;tmp_p->next_p = del_node->next_p;if (del_node->next_p != NULL)del_node->next_p->prev_p = tmp_p;free(del_node);return 0;}tmp_p = tmp_p->next_p;}return -1;
}

8. 修改操作

int DLINK_LIST_ChangeNodeData(node_p head_node, int data, int change_data)
{if (DLINK_LIST_IfEmpty(head_node))return -1;node_p tmp_p = head_node->next_p;while (tmp_p != NULL) {if (tmp_p->data == data) {tmp_p->data = change_data;return 0;}tmp_p = tmp_p->next_p;}return -1;
}

9. 销毁链表

void DLINK_LIST_UnInit(node_p head_node)
{if (DLINK_LIST_IfEmpty(head_node)) {free(head_node);return;}node_p tmp_p = head_node->next_p;node_p next_p;while (tmp_p != NULL) {next_p = tmp_p->next_p;free(tmp_p);tmp_p = next_p;}free(head_node);
}

三、通用型容器设计

1. 基本概念

通用型容器是指不关心存储数据的具体类型,只关注数据间的逻辑关系和相应操作的数据结构。它提供了一套可以处理任何数据类型的通用API。

实现方式

  • 让用户提供数据类型(C++ STL方式)
  • 将数据从容器中剥离(Linux内核链表方式)

在C语言中,通常采用第二种方式,通过宏定义实现类型切换。

2. 通用型节点设计

// 通过宏定义选择数据类型
#define STU_DATA 1
#define MED_DATA 0#if STU_DATA#define DATATYPE stu_t
#elif MED_DATA#define DATATYPE med_t
#endiftypedef DATATYPE datatype;typedef struct node
{datatype data;          // 通用数据域struct node* prev_p;    // 前驱指针struct node* next_p;    // 后继指针
} node_t, *node_p;

3. 双向循环链表特点

  • 头节点的prev和next指针都指向自己
  • 判断空链表:head_node->prev_p == head_node && head_node->next_p == head_node
  • 遍历时以再次遇到头节点为结束条件

4. 通用型容器操作特点

  • 插入、删除、遍历操作与普通双向链表类似
  • 数据比较和操作需要用户自定义函数
  • 通过宏切换支持多种数据类型

四、实战应用建议

1. 学习重点

  • 理解指针操作和内存管理
  • 掌握双向链表的插入、删除原理
  • 学会通用型容器的设计思想

2. 常见错误

  • 忘记处理边界条件(空链表、首尾节点)
  • 内存泄漏(未正确释放节点)
  • 指针操作错误(造成链表断裂)

3. 调试技巧

  • 画图辅助理解指针变化
  • 使用调试器逐步跟踪指针操作
  • 添加打印语句输出链表状态

五、总结

双向链表是基础数据结构中的重要组成部分,相比单向链表提供了更大的灵活性。通用型容器设计则体现了数据结构设计的抽象思维,通过将数据与结构分离,实现了代码的复用和扩展性。

关键记忆点

  1. 双向链表每个节点有两个指针,分别指向前后节点
  2. 头节点不存储数据,用于简化操作
  3. 插入删除操作需要仔细处理指针指向
  4. 通用容器通过数据类型抽象实现代码复用
  5. 循环链表通过头节点自我指向实现循环特性

掌握双向链表和通用型容器的设计与实现,不仅能够加深对数据结构的理解,也为学习更复杂的数据结构和算法打下坚实基础。


练习题

  1. 实现一个双向循环链表,支持基本操作
  2. 使用通用型容器实现医疗挂号管理系统
  3. 比较双向链表与单向链表的性能差异
http://www.dtcms.com/a/392056.html

相关文章:

  • NodeRAG检索知识图谱复杂数据的启发
  • 卡尔曼滤波对非线性公式建模的详细步骤
  • Microsoft 365 中的 Entitlement Management(基础版)功能深度解析
  • 本科期间的技术回忆(流水账记录)
  • zotero和小绿鲸联合使用
  • Linux系统之logrotate的基本使用
  • 硬核突破!基于 ComfyUI + pyannote 实现 infiniteTalk 多轮对话数字人:从语音端点检测到上下文感知的闭环
  • 【LeetCode 每日一题】2197. 替换数组中的非互质数
  • 城市水资源与水环境:植被如何重塑地球水循环?
  • TransBench:阿里国际等推出的多语言翻译评测体系
  • Windows启动Minio服务
  • 技术原理与癌症筛查的适配性问题
  • 集合通信算法总结
  • AutoDL四周年 ,学会使用AutoDL
  • nginx如果启动失败怎么办?
  • linux使用gunzip来解压.gz文件
  • 硬件(十五)LCD
  • B3clf: 最新的精准预测药物血脑屏障透过能力的开源app
  • 第八章 惊喜10 分享+
  • SMMU 软件指南
  • [x-cmd] Windows 安装和使用 x-cmd 的方法
  • 17.渗透-.Linux基础命令(九)-Linux权限管理(chmod修改文件权限)
  • 测量交流电压,测量直流电压,兼容,ADC采样转换计算有效值
  • 红黑树封装实现map set
  • EMQX和MQTTX的安装
  • AI 大模型入门 四:检索增强生成(RAG),自动生成精准用例!
  • EDR与MITRE ATTCK 详解
  • 特征值和特征向量
  • Gridview:让 HPC 作业管理真正“看得见、点得着、跑得快”
  • C++/初识