双向循环链表完整实现与详解
双向循环链表完整实现与详解
本文将详细解析双向循环链表的设计思路、实现原理以及完整代码实现。通过本文,你将掌握双向循环链表的核心操作和实现技巧。
一、双向循环链表概述
双向循环链表是一种特殊的链表结构,具有以下特点:
- 双向性:每个节点包含指向前驱和后继的指针
- 循环性:尾节点的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后面插入20
printf("插入后: ");
ListPrint(list); // 输出: 10 <-> 20 <-> 30
// 测试删除
DelHeadNode(list); // 删除头节点(10)
printf("删除头节点后: ");
ListPrint(list); // 输出: 20 <-> 30
DelTailNode(list); // 删除尾节点(30)
printf("删除尾节点后: ");
ListPrint(list); // 输出: 20
InstTailNode(list, 40);
InstTailNode(list, 50);
printf("添加节点后: ");
ListPrint(list); // 输出: 20 <-> 40 <-> 50
DelSpecNode(list, 40); // 删除40
printf("删除40后: ");
ListPrint(list); // 输出: 20 <-> 50
// 释放链表
FreeList(list);
return 0;
}
测试结果:
插入后: 链表内容: 10 <-> 20 <-> 30
删除头节点后: 链表内容: 20 <-> 30
删除尾节点后: 链表内容: 20
添加节点后: 链表内容: 20 <-> 40 <-> 50
删除40后: 链表内容: 20 <-> 50
四、双向循环链表的优势
- 高效的头尾操作:头插、尾插、头删、尾删都是O(1)时间复杂度
- 双向遍历:可以从任意节点向前或向后遍历
- 循环结构:没有明确的起点和终点,适合循环访问场景
- 空间效率:相比数组不需要预先分配固定大小
五、应用场景
- 实现高效的双端队列(Deque)
- 音乐播放器的播放列表(循环播放)
- 浏览器历史记录(前进/后退)
- 内存管理中的页面置换算法
- 游戏开发中的对象池管理
六、总结
本文详细介绍了双向循环链表的实现原理和核心操作:
- 使用头节点简化边界条件处理
- 头插法和尾插法的高效实现
- 完整的删除操作(头删、尾删、指定删除)
- 安全的内存管理
- 详尽的测试用例
双向循环链表是计算机科学中重要的基础数据结构,掌握其原理和实现对于深入理解数据结构与算法具有重要意义。
完整代码已在上文提供,可直接复制使用。实际应用中可根据需求扩展功能,如链表反转、节点查找、链表合并等操作。

浙公网安备 33010602011771号