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

双向循环链表完整实现与详解

双向循环链表完整实现与详解

本文将详细解析双向循环链表的设计思路、实现原理以及完整代码实现。通过本文,你将掌握双向循环链表的核心操作和实现技巧。

一、双向循环链表概述

双向循环链表是一种特殊的链表结构,具有以下特点:

  1. 双向性:每个节点包含指向前驱和后继的指针
  2. 循环性:尾节点的next指向头节点,头节点的prev指向尾节点
  3. 高效操作:支持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;
}

操作流程

  1. 创建新节点
  2. 处理空链表情况
  3. 非空链表时:
    • 新节点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;
}

操作流程

  1. 创建新节点
  2. 处理空链表情况
  3. 非空链表时:
    • 原尾节点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);
}

释放流程

  1. 遍历释放所有数据节点
  2. 最后释放头节点
  3. 防止内存泄漏

三、测试用例

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

四、双向循环链表的优势

  1. 高效的头尾操作:头插、尾插、头删、尾删都是O(1)时间复杂度
  2. 双向遍历:可以从任意节点向前或向后遍历
  3. 循环结构:没有明确的起点和终点,适合循环访问场景
  4. 空间效率:相比数组不需要预先分配固定大小

五、应用场景

  1. 实现高效的双端队列(Deque)
  2. 音乐播放器的播放列表(循环播放)
  3. 浏览器历史记录(前进/后退)
  4. 内存管理中的页面置换算法
  5. 游戏开发中的对象池管理

六、总结

本文详细介绍了双向循环链表的实现原理和核心操作:

  1. 使用头节点简化边界条件处理
  2. 头插法和尾插法的高效实现
  3. 完整的删除操作(头删、尾删、指定删除)
  4. 安全的内存管理
  5. 详尽的测试用例

双向循环链表是计算机科学中重要的基础数据结构,掌握其原理和实现对于深入理解数据结构与算法具有重要意义。

完整代码已在上文提供,可直接复制使用。实际应用中可根据需求扩展功能,如链表反转、节点查找、链表合并等操作。

http://www.sczhlp.com/news/1305/

相关文章:

  • JAVA学习
  • CSS 线性渐变
  • VMware ESXi 8.0U3g 发布 - 领先的裸机 Hypervisor
  • 装机软件记录
  • day3_javascript1
  • day4_javascript2
  • 电化学
  • 亚马逊AutoML论文获最佳论文奖
  • 前端加密实现
  • MX galaxy Day16
  • 30天总结-第二十八天
  • 金华の第二场模拟赛
  • [Unity] 项目的一些系统架构思想
  • 多github账号的仓库配置
  • Project 2024 专业增强版安装激活步骤(附安装包)2025最新详细教程
  • MX galaxy Day15
  • Plant Com | 将基因编辑与组学、人工智能和先进农业技术相结合以提高作物产量
  • PhenoAssistant:一个用于自动植物表型分析的人工智能系统
  • 在Docker中,可以在一个容器中同时运行多个应用进程吗?
  • Computomics:利用先进的机器学习实现预测性植物育种
  • 在运维工作中,Docker 与 Kvm 有何区别?
  • 利用分子与数量遗传学最大化作物改良的遗传增益
  • 在运维工作中,详细说一下 Docker 有什么作用?
  • 7.29总结
  • busybox的编译简介
  • 基因组辅助作物改良
  • 洛谷题解:P1514 [NOIP 2010 提高组] 引水入城
  • 如何利用机器学习构建种质资源/品种分子鉴定系统?
  • 进度
  • 科学通报 | 万向元:生物育种技术助力作物杂种优势利用