双向链表与通用型容器
一、双向链表基本概念
双向链表是一种常见的数据结构,每个节点包含两个指针:一个指向前一个节点(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. 调试技巧
- 画图辅助理解指针变化
- 使用调试器逐步跟踪指针操作
- 添加打印语句输出链表状态
五、总结
双向链表是基础数据结构中的重要组成部分,相比单向链表提供了更大的灵活性。通用型容器设计则体现了数据结构设计的抽象思维,通过将数据与结构分离,实现了代码的复用和扩展性。
关键记忆点:
- 双向链表每个节点有两个指针,分别指向前后节点
- 头节点不存储数据,用于简化操作
- 插入删除操作需要仔细处理指针指向
- 通用容器通过数据类型抽象实现代码复用
- 循环链表通过头节点自我指向实现循环特性
掌握双向链表和通用型容器的设计与实现,不仅能够加深对数据结构的理解,也为学习更复杂的数据结构和算法打下坚实基础。
练习题:
- 实现一个双向循环链表,支持基本操作
- 使用通用型容器实现医疗挂号管理系统
- 比较双向链表与单向链表的性能差异