双向循环链表完整实现与详解
本文将详细解析双向循环链表的设计思路、实现原理以及完整代码实现。通过本文,你将掌握双向循环链表的核心操作和实现技巧。
一、双向循环链表概述
双向循环链表是一种特殊的链表结构,具有以下特点:
- 双向性:每个节点包含指向前驱和后继的指针
- 循环性:尾节点的next指向头节点,头节点的prev指向尾节点
- 高效操作:支持O(1)时间复杂度的头插、尾插、头删、尾删操作
数据结构定义
typedef int TypData;typedef struct DoubleListNode {TypData data; // 数据域struct DoubleListNode *prev; // 指向前一个节点的指针struct DoubleListNode *next; // 指向下一个节点的指针
} DListNode;
二、核心功能实现
1. 头节点创建
DListNode * CreHeadNode() {DListNode *Head = (DListNode *)calloc(1, sizeof(DListNode));if (NULL == Head) {perror("创建头节点失败");exit(EXIT_FAILURE);}Head->prev = Head; // 初始化为空链表(指向自身)Head->next = Head; // 初始化为空链表(指向自身)return Head;
}
关键点:
- 使用
calloc
分配内存并初始化为0 - 头节点的prev和next指针都指向自身
- 错误处理确保内存分配失败时程序安全退出
2. 新节点创建
DListNode * CreaOneNode(TypData data) {DListNode *Node = (DListNode *)calloc(1, sizeof(DListNode));if (NULL == Node) {perror("创建节点失败");exit(EXIT_FAILURE);}Node->data = data;Node->prev = NULL;Node->next = NULL;return Node;
}
注意:
- 新节点的prev和next指针初始化为NULL
- 后续插入操作会正确设置这些指针
3. 链表判空
bool IsEmpty(DListNode *List) {return (List->next == List); // 判断是否指向自身
}
原理:
- 当头节点的next指向自身时,链表为空
- 这是循环链表特有的判空方式
4. 头插法
bool InstHeadNode(DListNode *Head, TypData data) {// 创建新节点DListNode *New = CreaOneNode(data);if (NULL == New) return false;if (IsEmpty(Head)) {// 空链表插入Head->next = New;Head->prev = New;New->prev = Head;New->next = Head;return true;}// 非空链表插入DListNode *first = Head->next;New->next = first;New->prev = Head;first->prev = New;Head->next = New;return true;
}
操作流程:
- 创建新节点
- 处理空链表情况
- 非空链表时:
- 新节点next指向原首节点
- 新节点prev指向头节点
- 原首节点prev指向新节点
- 头节点next指向新节点
时间复杂度:O(1)
5. 尾插法
bool InstTailNode(DListNode *Head, TypData data) {DListNode *New = CreaOneNode(data);if (NULL == New) return false;if (IsEmpty(Head)) {// 空链表插入Head->next = New;Head->prev = New;New->prev = Head;New->next = Head;return true;}// 非空链表插入DListNode *last = Head->prev;last->next = New;New->prev = last;New->next = Head;Head->prev = New;return true;
}
操作流程:
- 创建新节点
- 处理空链表情况
- 非空链表时:
- 原尾节点next指向新节点
- 新节点prev指向原尾节点
- 新节点next指向头节点
- 头节点prev指向新节点
时间复杂度:O(1)
6. 指定位置插入
bool InstDataNode(DListNode *Head, TypData NewData, TypData targetData) {if (IsEmpty(Head)) {return false;}DListNode *p = Head->next;do {if (p->data == targetData) {DListNode *New = CreaOneNode(NewData);if (NULL == New) return false;// 插入操作New->next = p->next;New->prev = p;p->next->prev = New;p->next = New;return true;}p = p->next;} while (p != Head);printf("未找到数据 %d,插入失败\n", targetData);return false;
}
特点:
- 在目标节点后插入新节点
- 需要遍历链表查找目标位置
- 时间复杂度:O(n)
7. 头节点删除
bool DelHeadNode(DListNode *Head) {if (IsEmpty(Head)) {printf("链表为空,删除失败\n");return false;}DListNode *delNode = Head->next;if (delNode->next == Head) { // 只有一个节点Head->next = Head;Head->prev = Head;} else {Head->next = delNode->next;delNode->next->prev = Head;}free(delNode);return true;
}
处理逻辑:
- 空链表直接返回
- 单节点链表:头节点指向自身
- 多节点链表:更新头节点的next指针和新的首节点的prev指针
8. 尾节点删除
bool DelTailNode(DListNode *Head) {if (IsEmpty(Head)) {printf("链表为空,删除失败\n");return false;}DListNode *delNode = Head->prev;if (delNode->prev == Head) { // 只有一个节点Head->next = Head;Head->prev = Head;} else {Head->prev = delNode->prev;delNode->prev->next = Head;}free(delNode);return true;
}
处理逻辑:
- 空链表直接返回
- 单节点链表:头节点指向自身
- 多节点链表:更新头节点的prev指针和新的尾节点的next指针
9. 指定节点删除
bool DelSpecNode(DListNode *Head, TypData data) {if (IsEmpty(Head)) {printf("链表为空,删除失败\n");return false;}DListNode *curr = Head->next;do {if (curr->data == data) {// 更新相邻节点的指针curr->prev->next = curr->next;curr->next->prev = curr->prev;// 更新头节点指针(如果删除的是首节点或尾节点)if (curr == Head->next) Head->next = curr->next;if (curr == Head->prev) Head->prev = curr->prev;free(curr);return true;}curr = curr->next;} while (curr != Head);printf("未找到数据 %d,删除失败\n", data);return false;
}
关键点:
- 需要遍历查找目标节点
- 更新相邻节点的指针
- 特殊处理删除首节点或尾节点的情况
10. 链表遍历打印
void ListPrint(DListNode *Head) {if (IsEmpty(Head)) {printf("链表为空\n");return;}DListNode *p = Head->next;printf("链表内容: ");do {printf("%d", p->data);p = p->next;if (p != Head) printf(" <-> ");} while (p != Head);printf("\n");
}
输出示例:
链表内容: 10 <-> 20 <-> 30
11. 链表内存释放
void FreeList(DListNode *Head) {if (Head == NULL) return;if (!IsEmpty(Head)) {DListNode *curr = Head->next;while (curr != Head) {DListNode *temp = curr;curr = curr->next;free(temp);}}free(Head);
}
释放流程:
- 遍历释放所有数据节点
- 最后释放头节点
- 防止内存泄漏
三、测试用例
int main() {// 创建头节点DListNode *list = CreHeadNode();// 测试插入InstHeadNode(list, 10); // 头插InstTailNode(list, 30); // 尾插InstDataNode(list, 20, 10); // 在10后面插入20printf("插入后: ");ListPrint(list); // 输出: 10 <-> 20 <-> 30// 测试删除DelHeadNode(list); // 删除头节点(10)printf("删除头节点后: ");ListPrint(list); // 输出: 20 <-> 30DelTailNode(list); // 删除尾节点(30)printf("删除尾节点后: ");ListPrint(list); // 输出: 20InstTailNode(list, 40);InstTailNode(list, 50);printf("添加节点后: ");ListPrint(list); // 输出: 20 <-> 40 <-> 50DelSpecNode(list, 40); // 删除40printf("删除40后: ");ListPrint(list); // 输出: 20 <-> 50// 释放链表FreeList(list);return 0;
}
测试结果:
插入后: 链表内容: 10 <-> 20 <-> 30
删除头节点后: 链表内容: 20 <-> 30
删除尾节点后: 链表内容: 20
添加节点后: 链表内容: 20 <-> 40 <-> 50
删除40后: 链表内容: 20 <-> 50
四、双向循环链表的优势
- 高效的头尾操作:头插、尾插、头删、尾删都是O(1)时间复杂度
- 双向遍历:可以从任意节点向前或向后遍历
- 循环结构:没有明确的起点和终点,适合循环访问场景
- 空间效率:相比数组不需要预先分配固定大小
五、应用场景
- 实现高效的双端队列(Deque)
- 音乐播放器的播放列表(循环播放)
- 浏览器历史记录(前进/后退)
- 内存管理中的页面置换算法
- 游戏开发中的对象池管理
六、总结
本文详细介绍了双向循环链表的实现原理和核心操作:
- 使用头节点简化边界条件处理
- 头插法和尾插法的高效实现
- 完整的删除操作(头删、尾删、指定删除)
- 安全的内存管理
- 详尽的测试用例
双向循环链表是计算机科学中重要的基础数据结构,掌握其原理和实现对于深入理解数据结构与算法具有重要意义。
完整代码已在上文提供,可直接复制使用。实际应用中可根据需求扩展功能,如链表反转、节点查找、链表合并等操作。