【数据结构】带头+双向+循环链表(DList)(增、删、查、改)详解
一、带头双向循环链表的定义和结构
1、定义
带头双向循环链表,有一个数据域和两个指针域。一个是前驱指针,指向其前一个节点;一个是后继指针,指向其后一个节点。
// 定义双向链表的节点
typedef struct ListNode
{LTDataType data; // 数据域struct ListNode* prev; // 前驱指针struct ListNode* next; // 后继指针
}ListNode;
2、结构
带头双向循环链表:在所有的链表当中 结构最复杂,一般用在 单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多 优势,实现反而简单了。
二、带头双向循环链表接口的实现
1、创建文件
- test.c(主函数、测试顺序表各个接口功能)
- List.c(带头双向循环链表接口函数的实现)
- List.h(带头双向循环链表的类型定义、接口函数声明、引用的头文件)

2、List.h 头文件代码
// List.h
// 带头+双向+循环链表增删查改实现
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>typedef int LTDataType;// 定义双向链表的节点
typedef struct ListNode
{LTDataType data; // 数据域struct ListNode* prev; // 前驱指针struct ListNode* next; // 后继指针
}ListNode;// 动态申请一个新节点
ListNode* BuyListNode(LTDataType x);
// 创建返回链表的头结点
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* plist);
// 双向链表打印
void ListPrint(ListNode* plist);
// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* plist);
// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* plist);
// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);
// 双向链表的判空
bool ListEmpty(ListNode* phead);
// 获取双向链表的元素个数
size_t ListSize(ListNode* phead);
三、在 List.c 上是西安各个接口函数
1、动态申请一个新结点
// 动态申请一个新节点
ListNode* BuyListNode(LTDataType x)
{ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));newnode->data = x;newnode->prev = NULL;newnode->next = NULL;return newnode;
}
2、创建返回链表的头结点(初始化头结点)
// 创建返回链表的头结点
ListNode* ListCreate()
{ListNode* phead = (ListNode*)malloc(sizeof(ListNode)); // 哨兵位头结点phead->next = phead;phead->prev = phead;return phead;
}
也可以用下面这个函数(道理一样):
// 初始化链表
void ListInit(ListNode** pphead)
{*pphead = BuyListNode(-1); // 动态申请一个头节点(*pphead)->prev = *pphead; // 前驱指针指向自己(*pphead)->next = *pphead; // 后继指针指向自己
}
头指针初始指向 NULL,初始化链表时,需要改变头指针的指向,使其指向头节点,所以这里需要传二级指针。
初始化带头双向循环链表,首先动态申请一个头结点,头结点的前驱和后继指针都指向自己,形成一个循环。
3、双向链表的销毁
// 双向链表的销毁
void ListDestroy(ListNode** pphead)
{assert(pphead);assert(*pphead);ListNode* cur = (*pphead)->next;while (cur != *pphead){ListNode* next = cur->next; // 记录cur的直接后继节点free(cur);cur = next;}free(*pphead); // 释放头节点*pphead = NULL; // 置空头指针
}
销毁链表,最后要将头指针 plist 置空,所以用了二级指针来接收。这里也可以用一级指针,但要在函数外面置空 plist 。
一级指针写法:
void ListDestroy(ListNode* phead) {assert(phead);ListNode* cur = phead->next;while (cur != phead){ListNode* next = cur->next;free(cur);cur = next;}free(phead);phead = NULL; }
4、双向链表的打印
// 打印双向链表
void ListPrint(ListNode* phead)
{assert(phead);ListNode* cur = phead->next; // 记录第一个节点printf("head <-> ");while (cur != phead){printf("%d <-> ", cur->data);cur = cur->next;}printf("head\n");
}
5、双向链表的尾插
// 双向链表尾插
void ListPushBack(ListNode* phead, LTDataType x)
{assert(phead); // 头指针不能为空/* ListNode* newnode = BuyListNode(x); // 动态申请一个节点ListNode* tail = phead->prev; // 记录尾节点tail->next = newnode; // 尾节点的后继指针指向新节点newnode->prev = tail; //2、新节点的前驱指针指向尾节点newnode->next = phead; // 新节点的后继指针指向头节点phead->prev = newnode; // 头节点的前驱指针指向新节点 */ListInsert(phead, x);
}
6、双向链表的尾删
// 双向链表的尾删
void ListPopBack(ListNode* phead)
{assert(phead);assert(phead->next != phead); // 只剩头节点时 链表为空 不能再继续删除/* ListNode* tail = phead->prev; // 记录尾节点ListNode* tailPrev = tail->prev; // 记录尾节点的直接前驱tailPrev->next = phead; // 尾节点的前驱节点的next指针指向头节点phead->prev = tailPrev; // 头节点的prev指针指向尾节点的前驱节点free(tail); // 释放尾节点 */ListErase(pHead->prev);
}

7、双向链表的头插
// 双向链表的头插
void ListPushFront(ListNode* phead, LTDataType x)
{assert(phead);/* ListNode* newnode = BuyListNode(x); // 申请新节点ListNode* pheadNext = phead->next; // 记录第一个节点// 头节点和新节点建立链接phead->next = newnode;newnode->prev = phead;// 新节点和第一个节点建立链接newnode->next = pheadNext;pheadNext->prev = newnode; */ListInsert(phead->next, x);
}
8、双向链表的头删
// 双向链表的头删
void ListPopFront(ListNode* phead)
{assert(phead);assert(phead->next != phead); // 只剩头节点时 链表为空 不能再继续删除/* ListNode* pheadNext = phead->next; // 记录第一个节点// 头节点和第一个节点的后继节点建立链接phead->next = pheadNext->next;pheadNext->next->prev = phead;free(pheadNext); // 头删 */ListErase(phead->next);
}

9、查找双向链表中的元素
// 在双向链表中查找元素,并返回该元素的地址
ListNode* ListFind(ListNode* phead, LTDataType x)
{assert(phead);ListNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur; //找到了 返回该元素的地址}cur = cur->next;}return NULL; //没找到 返回NULL
}
10、在指定pos位置之前插入元素
// 在指定pos位置之前插入元素
void ListInsert(ListNode* pos, LTDataType x)
{assert(pos);ListNode* newnode = BuyListNode(x); // 申请一个节点ListNode* posPrev = pos->prev; // 记录pos的直接前驱// pos的直接前驱和新节点建立链接posPrev->next = newnode;newnode->prev = posPrev;// 新节点和pos建立链接newnode->next = pos;pos->prev = newnode;
}
实现了该函数后,可以尝试改进头插函数(pos相当于链表的第一个节点)和尾插函数(pos相当于链表的头节点),这样写起来更简便。
11、删除指定pos位置的元素
// 删除指定pos位置的元素
void ListErase(ListNode* pos)
{assert(pos);ListNode* posPrev = pos->prev; // 记录pos的直接前驱ListNode* posNext = pos->next; // 记录pos的直接后继// pos的直接前驱和直接后继建立链接posPrev->next = posNext;posNext->prev = posPrev;free(pos); // 释放pos位置的元素//pos = NULL;
}
实现了该函数后,可以尝试改进头删函数(pos相当于链表的第一个节点)和尾删函数(pos相当于链表的最后一个节点),这样写起来更简便。

12、双向链表的判空
// 双向链表的判空
bool ListEmpty(ListNode* phead)
{ assert(phead);return phead->next == phead; //为空 返回ture 否则返回false
}
13、获取双向链表的元素个数
// 获取双向链表的元素个数
size_t ListSize(ListNode* phead)
{assert(phead);size_t size = 0;ListNode* cur = phead->next; // 记录第一个节点while (cur != phead){size++;cur = cur->next;}return size;
}
四、代码整合
// List.c
#include "List.h"// 动态申请一个新节点
ListNode* BuyListNode(LTDataType x)
{ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));newnode->data = x;newnode->prev = NULL;newnode->next = NULL;return newnode;
}// 创建返回链表的头结点
ListNode* ListCreate()
{ListNode* phead = (ListNode*)malloc(sizeof(ListNode)); // 哨兵位头结点phead->next = phead;phead->prev = phead;return phead;
}// 双向链表的销毁
void ListDestroy(ListNode** pphead)
{assert(pphead);assert(*pphead);ListNode* cur = (*pphead)->next;while (cur != *pphead){ListNode* next = cur->next; // 记录cur的直接后继节点free(cur);cur = next;}free(*pphead); // 释放头节点*pphead = NULL; // 置空头指针
}// 打印双向链表
void ListPrint(ListNode* phead)
{assert(phead);ListNode* cur = phead->next; // 记录第一个节点printf("head <-> ");while (cur != phead){printf("%d <-> ", cur->data);cur = cur->next;}printf("head\n");
}// 双向链表尾插
void ListPushBack(ListNode* phead, LTDataType x)
{assert(phead); // 头指针不能为空ListInsert(phead, x);
}// 双向链表的尾删
void ListPopBack(ListNode* phead)
{assert(phead);assert(phead->next != phead); // 只剩头节点时 链表为空 不能再继续删除ListErase(pHead->prev);
}// 双向链表的头插
void ListPushFront(ListNode* phead, LTDataType x)
{assert(phead);ListInsert(phead->next, x);
}// 双向链表的头删
void ListPopFront(ListNode* phead)
{assert(phead);assert(phead->next != phead); // 只剩头节点时 链表为空 不能再继续删除ListErase(phead->next);
}// 在双向链表中查找元素,并返回该元素的地址
ListNode* ListFind(ListNode* phead, LTDataType x)
{assert(phead);ListNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur; //找到了 返回该元素的地址}cur = cur->next;}return NULL; //没找到 返回NULL
}// 在指定pos位置之前插入元素
void ListInsert(ListNode* pos, LTDataType x)
{assert(pos);ListNode* newnode = BuyListNode(x); // 申请一个节点ListNode* posPrev = pos->prev; // 记录pos的直接前驱// pos的直接前驱和新节点建立链接posPrev->next = newnode;newnode->prev = posPrev;// 新节点和pos建立链接newnode->next = pos;pos->prev = newnode;
}// 删除指定pos位置的元素
void ListErase(ListNode* pos)
{assert(pos);ListNode* posPrev = pos->prev; // 记录pos的直接前驱ListNode* posNext = pos->next; // 记录pos的直接后继// pos的直接前驱和直接后继建立链接posPrev->next = posNext;posNext->prev = posPrev;free(pos); // 释放pos位置的元素//pos = NULL;
}// 双向链表的判空
bool ListEmpty(ListNode* phead)
{ assert(phead);return phead->next == phead; //为空 返回ture 否则返回false
}// 获取双向链表的元素个数
size_t ListSize(ListNode* phead)
{assert(phead);size_t size = 0;ListNode* cur = phead->next; // 记录第一个节点while (cur != phead){size++;cur = cur->next;}return size;
}
相关文章:
【数据结构】带头+双向+循环链表(DList)(增、删、查、改)详解
一、带头双向循环链表的定义和结构 1、定义 带头双向循环链表,有一个数据域和两个指针域。一个是前驱指针,指向其前一个节点;一个是后继指针,指向其后一个节点。 // 定义双向链表的节点 typedef struct ListNode {LTDataType dat…...
接口自动化测试平台
下载了大神的EasyTest项目demo修改了下<https://testerhome.com/topics/12648 原地址>。也有看另一位大神的HttpRunnerManager<https://github.com/HttpRunner/HttpRunnerManager 原地址>,由于水平有限,感觉有点复杂~~~ 【整整200集】超超超…...
【物联网】微信小程序接入阿里云物联网平台
微信小程序接入阿里云物联网平台 一 阿里云平台端 1.登录阿里云 阿里云物联网平台 点击进入公共实例,之前没有的点进去申请 2.点击产品,创建产品 3.产品名称自定义,按项目选择类型,节点类型选择之恋设备,联网方式W…...
PKG内容查看工具:Suspicious Package for Mac安装教程
Suspicious Package Mac版是一款Mac平台上的查看 PKG 程序包内信息的应用,Suspicious Package Mac版支持查看全部包内全部文件,比如需要运行的脚本,开发者,来源等等。 suspicious package mac使用简单,只需在选择pkg安…...
第16节:R语言医学分析实例:肺切除手术的Apriori关联规则分析
关联规则 肺切除手术的Apriori关联规则分析。 分析的目的是确定患有肺癌并需要接受肺切除术的患者的共病症状。 了解哪些症状是共病的可以帮助改善患者护理和药物处方。 分析类型是关联规则学习,通过探索变量之间的关联或频繁项集,尝试在大型数据集中找到见解和隐藏关系(H…...
ChatGPT+MidJourney 3分钟生成你的动画故事
chatgpt是真的火了,chatgpt产生了一个划时代的意义——自chatgpt起,AI是真的要落地了。 chatgpt能做的事情太多了,多到最初开发模型的程序员自己,也没法说得清楚chatgpt都能做啥,似乎只要你能想得到,它都有…...
在CSDN学Golang云原生(Kubernetes Pod调度)
一,NodeSelector定向调度 在 Kubernetes 中,可以使用 NodeSelector 字段来指定 Pod 调度到哪些节点上运行。NodeSelector 是一个键值对的 map,其中键是节点的标签名,值是标签值。具体步骤如下: 在节点上添加标签 首…...
Rust vs Go:常用语法对比(七)
题图来自 Go vs Rust: Which will be the top pick in programming?[1] 121. UDP listen and read Listen UDP traffic on port p and read 1024 bytes into buffer b. 听端口p上的UDP流量,并将1024字节读入缓冲区b。 import ( "fmt" "net&qu…...
【HarmonyOS】API6使用storage实现轻量级数据存储
写在前面 本篇内容基于API6 JS语言进行开发,通过结合轻量级数据存储开发指导的文档,帮助大家完成一个实际的代码案例,通过这个小案例,可以实现简单数据的存储。 参考文档:文档中心 1、页面布局 首先我们编写一个简单…...
Python Flask构建微信小程序订餐系统 (十二)
🔥 创建切换商品分类状态的JS文件 🔥 ; var food_act_ops={init:function(){this.eventBind();},eventBind:function(){//表示作用域var that = this;$(".wrap_search select[name=status]").change(function(){$(".wrap_search").submit();});$(&qu…...
C++——模板的作用2:特例化
目录 模板的形式: 一.模板的多参数应用: 例: 错误使用1:使用不标准的模板形参表 编辑 错误使用2:使用变量作为实参传递给函数模板 二.模板的特例化: 类模板: 针对模板的特化步骤&am…...
Python Web开发技巧VII
目录 装饰器inject_serializer 装饰器atomic rebase git 清理add的数据 查看git的当前工作目录 makemigrations文件名称 action(detailTrue, methods["GET"]) 如何只取序列化器的一个字段进行返回 Response和JsonResponse有什么区别 序列化器填表和单字段如…...
LaTex4【下载模板、引入文献】
下载latex模板:(模板官网一般都有,去找) 我这随便找了一个: 下载得到一个压缩包,然后用overleaf打开👇: (然后改里面的内容就好啦) 另外,有很多在线的数学公式编辑器&am…...
【VSCode部署模型】导出TensorFlow2.X训练好的模型信息
参考tensorflow2.0 C加载python训练保存的pb模型 经过模型训练及保存,我们得到“OptimalModelDataSet2”文件夹,模型的保存方法(.h5或.pb文件),参考【Visual Studio Code】c/c部署tensorflow训练的模型 其中“OptimalModelDataSet2”文件夹保…...
windows环境下,安装elasticsearch
目录 前言准备安装 jdk 安装nodejsElasticSearch下载ElasticSearch-head 下载 安装ElasticSearch安装ElasticSearch-head插件设置用户名密码访问ElasticSearch 默认用户名和密码参考 前言 win10elasticsearch 8.9.0 准备 安装 jdk ElasticSearch 是基于lucence开发的&#…...
Elasticsearch入门笔记(一)
环境搭建 Elasticsearch是搜索引擎,是常见的搜索工具之一。 Kibana 是一个开源的分析和可视化平台,旨在与 Elasticsearch 合作。Kibana 提供搜索、查看和与存储在 Elasticsearch 索引中的数据进行交互的功能。开发者或运维人员可以轻松地执行高级数据分析…...
记一次安装nvm切换node.js版本实例详解
最后效果如下: 背景:由于我以前安装过node.js,后续想安装nvm将node.js管理起来。 问题:nvm-use命令行运行成功,但是nvm-list显示并没有成功。 原因:因为安装过node.js,所以原先的node.js不收n…...
生态共建丨YashanDB与构力科技完成兼容互认证
近日,深圳计算科学研究院崖山数据库系统YashanDB V22.2与北京构力科技有限公司BIMBase云平台完成兼容性互认证。经严格测试,双方产品完全兼容、运行稳定。 崖山数据库系统YashanDB是深算院自主研发设计的新型数据库系统,融入原创理论…...
React从入门到实战-react脚手架,消息订阅与发布
创建项目并启动 全局安装 npm install -g create-react-app切换到想创建项目的目录,使用命令:create-react-app 项目名称 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存中…(iQ6hEUgAABpQAAAD1CAYAAABeIRZoAAAAAXNSR0IArs4c6QAAIABJREFUe…...
从零构建深度学习推理框架-1 简介和Tensor
源代码作者:https://github.com/zjhellofss 本文仅作为个人学习心得领悟 ,将原作品提炼,更加适合新手 什么是推理框架? 深度学习推理框架用于对已训练完成的神经网络进行预测,也就是说,能够将深度训练框…...
第19节 Node.js Express 框架
Express 是一个为Node.js设计的web开发框架,它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用,和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...
【Python】 -- 趣味代码 - 小恐龙游戏
文章目录 文章目录 00 小恐龙游戏程序设计框架代码结构和功能游戏流程总结01 小恐龙游戏程序设计02 百度网盘地址00 小恐龙游戏程序设计框架 这段代码是一个基于 Pygame 的简易跑酷游戏的完整实现,玩家控制一个角色(龙)躲避障碍物(仙人掌和乌鸦)。以下是代码的详细介绍:…...
解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...
页面渲染流程与性能优化
页面渲染流程与性能优化详解(完整版) 一、现代浏览器渲染流程(详细说明) 1. 构建DOM树 浏览器接收到HTML文档后,会逐步解析并构建DOM(Document Object Model)树。具体过程如下: (…...
NFT模式:数字资产确权与链游经济系统构建
NFT模式:数字资产确权与链游经济系统构建 ——从技术架构到可持续生态的范式革命 一、确权技术革新:构建可信数字资产基石 1. 区块链底层架构的进化 跨链互操作协议:基于LayerZero协议实现以太坊、Solana等公链资产互通,通过零知…...
AspectJ 在 Android 中的完整使用指南
一、环境配置(Gradle 7.0 适配) 1. 项目级 build.gradle // 注意:沪江插件已停更,推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...
代码规范和架构【立芯理论一】(2025.06.08)
1、代码规范的目标 代码简洁精炼、美观,可持续性好高效率高复用,可移植性好高内聚,低耦合没有冗余规范性,代码有规可循,可以看出自己当时的思考过程特殊排版,特殊语法,特殊指令,必须…...
群晖NAS如何在虚拟机创建飞牛NAS
套件中心下载安装Virtual Machine Manager 创建虚拟机 配置虚拟机 飞牛官网下载 https://iso.liveupdate.fnnas.com/x86_64/trim/fnos-0.9.2-863.iso 群晖NAS如何在虚拟机创建飞牛NAS - 个人信息分享...
为什么要创建 Vue 实例
核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...
TSN交换机正在重构工业网络,PROFINET和EtherCAT会被取代吗?
在工业自动化持续演进的今天,通信网络的角色正变得愈发关键。 2025年6月6日,为期三天的华南国际工业博览会在深圳国际会展中心(宝安)圆满落幕。作为国内工业通信领域的技术型企业,光路科技(Fiberroad&…...








