数据结构-带头双向循环链表的实现
前言
带头双向循环链表是一种重要的数据结构,它的结构是很完美的,它弥补了单链表的许多不足,让我们一起来了解一下它是如何实现的吧!
1.节点的结构
它的节点中存储着数据和两个指针,一个指针_prev用来记录前一个节点的地址,另一个指针_next 用来记录后一个节点的地址。

typedef int LTDataType;
typedef struct ListNode
{struct ListNode* _next;struct ListNode* _prev;LTDataType _data;
}ListNode;
2.尾部的插入和删除的实现
由于这是带头循环的双向链表,所以尾插只需要在它的头结点的_prev 处进行插入,然后重新链接就可以了。如图:
如果只有一个头结点,插入也是一样的。
void ListPushBack(ListNode* phead, LTDataType data)//尾插
{assert(phead);ListNode* newNode = BuyListNode(data);//申请新节点ListNode* tail = phead->_prev;//找尾结点//链接新节点和尾结点tail->_next = newNode;newNode->_prev = tail;//与头结点进行链接phead->_prev = newNode;newNode->_next = phead;
}
尾部的删除,首先需要找到链表的尾和尾的前一个节点,删除尾结点之后,将前一个节点进与头结点进行链接,如图:

void ListPopBack(ListNode* phead)//尾删除
{//确保指针不为空assert(phead);assert(phead->_next != phead);//保留头结点//找尾ListNode* tail = phead->_prev;ListNode* newTail = tail->_prev;//找到新的尾结点//删除旧的尾结点free(tail);//重新链接头尾节点newTail->_next = phead;phead->_prev = newTail;
}
3.头部插入和删除的实现
头部的插入,删除和尾部的插入,删除类似,需要注意的是删除的不是 头结点,是存储有效数据的第一个节点,插入数据也是在第一个有效数据节点和头结点之间插入。如图:

void ListPushFront(ListNode* phead, LTDataType data)//头插
{//确保指针不为空assert(phead);//申请新的节点ListNode* newNode = BuyListNode(data);//进行链接ListNode* realHead = phead->_next;realHead->_prev = newNode;newNode->_next = realHead;phead->_next = newNode;newNode->_prev = phead;
}
void ListPopFront(ListNode* phead)//头删插
{//指针存在assert(phead);//并且确保不能删除头结点assert(phead->_next != phead);//找到真正的头ListNode* realHead = phead->_next;ListNode* realHeadNext = realHead->_next;//删除头节点free(realHead);//重新链接phead->_next = realHeadNext;realHeadNext->_prev = phead;
}
4.任意位置的插入和删除
在任意位置进行插入和删除,需要知道节点的指针,插入或者删除节点之后需要 更新节点的链接关系。如图:
void ListInsert(ListNode* pos, LTDataType data)//pos位置之前的插入
{assert(pos);//确保指针有效ListNode* newNode = BuyListNode(data);//申请节点//进行链接ListNode* prev = pos->_prev;ListNode* next = pos;prev->_next = newNode;newNode->_prev = prev;newNode->_next = next;next->_prev = newNode;
}
void ListErase(ListNode* pos)//pos位置的删除
{//确保指针有效assert(pos);ListNode* next = pos->_next;ListNode* prev = pos->_prev;//删除pos所指向的节点free(pos);//进行重新链接prev->_next = next;next->_prev = prev;
}
对任意位置的插入和删除进行测试时,可以通过复用接口来实现,头插尾插,头删尾删都可以调用这两个接口来实现,如下:
void ListPushBack(ListNode* phead, LTDataType data)//尾插
{ListInsert(phead, data);
}
void ListPopBack(ListNode* phead)//尾删除
{ListErase(phead->_prev);
}
void ListPushFront(ListNode* phead, LTDataType data)//头插
{ListInsert(phead->_next,data);
}
void ListPopFront(ListNode* phead)//头删插
{ListErase(phead->_next);
}
5.链表的初始化和删除
带头的双向循环链表初始化的时候就需要申请内存给头节点。
ListNode* BuyListNode(LTDataType data)//申请节点
{ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));if (newNode == NULL){printf("申请空间失败\n");exit(-1);}newNode->_data = data;return newNode;
}
void ListInit(ListNode** pphead)//初始化链表
{*pphead = BuyListNode(0);//申请头结点//并且初始化(*pphead)->_next = *pphead;(*pphead)->_prev = *pphead;
}
//清理链表
void ListClear(ListNode* phead)
{assert(phead);//确保链表不为空assert(phead->_next != phead);//除了确保不清理头结点ListNode* cur = phead->_next;while (cur != phead){ListNode* clearNode = cur;cur = cur->_next;free(clearNode);}
}
void ListDestory(ListNode** ppHead)//摧毁链表
{assert(*ppHead);//确保指针不为空ListClear(*ppHead);free(*ppHead);//释放头结点
}
6.查找并修改链表的节点的数据
查找和修改是一起的,实现查找就可以实现 修改链表的值。
ListNode* ListFind(ListNode* phead, LTDataType data)//查找链表
{assert(phead);ListNode* cur = phead->_next;while (cur != phead){if (cur->_data == data)return cur;cur = cur->_next;}return NULL;//找不到返回NULL
}
void ListTest4()
{//查找和修改的测试ListNode* pHead = NULL;ListInit(&pHead);ListPushFront(pHead, 1);ListPushFront(pHead, 2);ListPushFront(pHead, 3);ListPushFront(pHead, 4);ListPushFront(pHead, 5);ListPushFront(pHead, 6);ListNode* node = ListFind(pHead, 3);//在链表中查找if (node){//修改链表的值node->_data = 90;}ListPrint(pHead);ListDestory(&pHead);
}
7.全部代码
//List.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode
{struct ListNode* _next;struct ListNode* _prev;LTDataType _data;
}ListNode;void ListPushBack(ListNode* phead, LTDataType data);//尾插void ListPopBack(ListNode* phead);//尾删除
void ListPushFront(ListNode* phead,LTDataType data);//头插
void ListPopFront(ListNode* phead);//头删插
ListNode* BuyListNode(LTDataType data);//申请节点
void ListInit(ListNode** pphead);//初始化链表void ListInsert(ListNode* pos, LTDataType data);//pos位置之前的插入void ListErase(ListNode* pos);//pos位置的删除
//清理链表
void ListClear(ListNode* phead);void ListDestory(ListNode** ppHead);//摧毁链表void ListPrint(ListNode* phead);//打印链表
ListNode* ListFind(ListNode* phead, LTDataType data);//查找链表
//List.c
#include"List.h"
void ListPushBack(ListNode* phead, LTDataType data)//尾插
{assert(phead);ListNode* newNode = BuyListNode(data);//申请新节点ListNode* tail = phead->_prev;//找尾结点//链接新节点和尾结点tail->_next = newNode;newNode->_prev = tail;//与头结点进行链接phead->_prev = newNode;newNode->_next = phead;
}
void ListPopBack(ListNode* phead)//尾删除
{//确保指针不为空assert(phead);assert(phead->_next != phead);//保留头结点//找尾ListNode* tail = phead->_prev;ListNode* newTail = tail->_prev;//找到新的尾结点//删除旧的尾结点free(tail);//重新链接头尾节点newTail->_next = phead;phead->_prev = newTail;
}
void ListPushFront(ListNode* phead, LTDataType data)//头插
{//确保指针不为空assert(phead);//申请新的节点ListNode* newNode = BuyListNode(data);//进行链接ListNode* realHead = phead->_next;realHead->_prev = newNode;newNode->_next = realHead;phead->_next = newNode;newNode->_prev = phead;
}
void ListPopFront(ListNode* phead)//头删插
{//指针存在assert(phead);//并且确保不能删除头结点assert(phead->_next != phead);//找到真正的头ListNode* realHead = phead->_next;ListNode* realHeadNext = realHead->_next;//删除头节点free(realHead);//重新链接phead->_next = realHeadNext;realHeadNext->_prev = phead;
}
ListNode* BuyListNode(LTDataType data)//申请节点
{ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));if (newNode == NULL){printf("申请空间失败\n");exit(-1);}newNode->_data = data;return newNode;
}
void ListInit(ListNode** pphead)//初始化链表
{*pphead = BuyListNode(0);//申请头结点//并且初始化(*pphead)->_next = *pphead;(*pphead)->_prev = *pphead;
}
void ListPrint(ListNode* phead)//打印链表
{assert(phead);ListNode* cur = phead->_next;while (cur != phead){printf("%d ", cur->_data);cur = cur->_next;}printf("\n");
}
void ListInsert(ListNode* pos, LTDataType data)//pos位置之前的插入
{assert(pos);//确保指针有效ListNode* newNode = BuyListNode(data);//申请节点//进行链接ListNode* prev = pos->_prev;ListNode* next = pos;prev->_next = newNode;newNode->_prev = prev;newNode->_next = next;next->_prev = newNode;
}
void ListErase(ListNode* pos)//pos位置的删除
{//确保指针有效assert(pos);ListNode* next = pos->_next;ListNode* prev = pos->_prev;//删除pos所指向的节点free(pos);//进行重新链接prev->_next = next;next->_prev = prev;
}
//清理链表
void ListClear(ListNode* phead)
{assert(phead);//确保链表不为空assert(phead->_next != phead);//除了确保不清理头结点ListNode* cur = phead->_next;while (cur != phead){ListNode* clearNode = cur;cur = cur->_next;free(clearNode);}
}
void ListDestory(ListNode** ppHead)//摧毁链表
{assert(*ppHead);//确保指针不为空ListClear(*ppHead);free(*ppHead);//释放头结点
}
ListNode* ListFind(ListNode* phead, LTDataType data)//查找链表
{assert(phead);ListNode* cur = phead->_next;while (cur != phead){if (cur->_data == data)return cur;cur = cur->_next;}return NULL;//找不到返回NULL
}
//test.c
#include"List.h"
void ListTest1()
{//尾插尾删的测试代码ListNode* pHead = NULL;ListInit(&pHead);ListPushBack(pHead, 1);ListPushBack(pHead, 2);ListPushBack(pHead, 3);ListPushBack(pHead, 4);ListPushBack(pHead, 5);ListPushBack(pHead, 6);ListPrint(pHead);ListPopBack(pHead);ListPopBack(pHead);ListPopBack(pHead);ListPopBack(pHead);ListPopBack(pHead);ListPopBack(pHead);//ListPopBack(pHead);}
void ListTest2()
{//头插头删的测试ListNode* pHead = NULL;ListInit(&pHead);ListPushFront(pHead, 1);ListPushFront(pHead, 2);ListPushFront(pHead, 3);ListPushFront(pHead, 4);ListPushFront(pHead, 5);ListPushFront(pHead, 6);ListPrint(pHead);ListPopFront(pHead);ListPopFront(pHead);ListPopFront(pHead);ListPopFront(pHead);ListPopFront(pHead);ListPopFront(pHead);ListPopFront(pHead);}
void ListTest3()
{//Destory和Clear的测试ListNode* pHead = NULL;ListInit(&pHead);ListPushFront(pHead, 1);ListPushFront(pHead, 2);ListPushFront(pHead, 3);ListPushFront(pHead, 4);ListPushFront(pHead, 5);ListPushFront(pHead, 6);ListDestory(&pHead);
}
int main()
{ListTest3();return 0;
}
8.测试代码
void ListTest1()
{//尾插尾删的测试代码ListNode* pHead = NULL;ListInit(&pHead);ListPushBack(pHead, 1);ListPushBack(pHead, 2);ListPushBack(pHead, 3);ListPushBack(pHead, 4);ListPushBack(pHead, 5);ListPushBack(pHead, 6);ListPrint(pHead);ListPopBack(pHead);ListPopBack(pHead);ListPopBack(pHead);ListPopBack(pHead);ListPopBack(pHead);ListPopBack(pHead);//ListPopBack(pHead);}
void ListTest2()
{//头插头删的测试ListNode* pHead = NULL;ListInit(&pHead);ListPushFront(pHead, 1);ListPushFront(pHead, 2);ListPushFront(pHead, 3);ListPushFront(pHead, 4);ListPushFront(pHead, 5);ListPushFront(pHead, 6);ListPrint(pHead);ListPopFront(pHead);ListPopFront(pHead);ListPopFront(pHead);ListPopFront(pHead);ListPopFront(pHead);ListPopFront(pHead);ListPopFront(pHead);}
void ListTest3()
{//Destory和Clear的测试ListNode* pHead = NULL;ListInit(&pHead);ListPushFront(pHead, 1);ListPushFront(pHead, 2);ListPushFront(pHead, 3);ListPushFront(pHead, 4);ListPushFront(pHead, 5);ListPushFront(pHead, 6);ListDestory(&pHead);
}
相关文章:
数据结构-带头双向循环链表的实现
前言 带头双向循环链表是一种重要的数据结构,它的结构是很完美的,它弥补了单链表的许多不足,让我们一起来了解一下它是如何实现的吧! 1.节点的结构 它的节点中存储着数据和两个指针,一个指针_prev用来记录前一个节点…...
android Ndk Jni动态注册方式以及静态注册
目录 一.静态注册方式 二.动态注册方式 三.源代码 一.静态注册方式 1.项目名\app\src\main下新建一个jni目录 2.在jni目录下,再新建一个Android.mk文件 写入以下配置 LOCAL_PATH := $(call my-dir)//获取当前Android.mk所在目录 inclu...
MySQL中的索引
1.2.MySQL中的索引 InnoDB存储引擎支持以下几种常见的索引:B树索引、全文索引、哈希索引,其中比较关键的是B树索引 1.2.1.B树索引 InnoDB中的索引自然也是按照B树来组织的,前面我们说过B树的叶子节点用来放数据的,但是放什么数…...
idea中如何处理飘红提示
idea中如何处理飘红提示 在写sql时,总是会提示各种错误 查找资料,大部分都是说关提示,这里把错误提示选择为None即可 关掉以后,也确实不显示任何提示了,但总有一种掩耳盗铃的感觉 这个sms表明明存在,但是还…...
Elasticsearch使用中出现的错误
Elasticsearch使用中出现的错误 1、分页查询异常 在分页的过程中出现了一个问题是当查询的数据超过10000条的时候报了异常: from size must be less than or equal to: [10000]这个问题最快捷的解决方式是增大窗口大小: curl -XPUT http://127.0.0.…...
【IMX6ULL驱动开发学习】01.编写第一个hello驱动+自动创建设备节点(不涉及硬件操作)
目录 一、驱动程序编写流程 二、代码编写 2.1 驱动程序hello_drv.c 2.2 测试程序 2.3 编写驱动程序的Makefile 三、上机实验 3.1 NFS 挂载 3.2 测试示例 一、驱动程序编写流程 构造file_operations结构体 在里面填充open/read/write/ioctl成员 注册file_operations结…...
决策规划仿真平台搭建
决策规划仿真平台搭建 自动驾驶决策规划算法第二章第一节 决策规划仿真平台搭建 这部分的主要难点在于多个软件的连通与适配,环境的搭建总是折磨人的,主要是 4 个软件,各软件版本如下 Visual Studio2017PreScan8.5.0CarSim2019.0MATLAB2019b…...
计算图像哈希SHA-512
1、MATLAB实现 计算图像哈希值SHA-512,在文献[1]提到的算法如下: % Example Code: Create an MD5 crypto-hash of an arbitrary string, "str" % Main class of interest: System.Security.Cryptography.HashAlgorithm% Example String to hash with MD5 %…...
Android之消除APP图标的白色边框
有问题的效果: 解决方案: 第一步:app右键—>new—>Image Asset 第二步:上传Logo图标,选择每种分辨率,预览看效果,选择Resize,可以微调 第三步:点击 Nextÿ…...
java线程的优先级、守护线程的概念
1.线程的调度 抢占式调度 非抢占式调度 1.1 抢占式调度 优先级越高,抢到cpu的概率越高 1.2 守护线程 守护线程,非守护线程。当其他的非守护线程执行完毕以后,守护线程会陆续结束。 守护线程的应用场景...
asp.net core 6.0 efcore +sqlserver增删改查的demo
asp.net core 6.0 efcore sqlserver增删改查的demo 下面是一个使用ASP.NET Core 5.0和Entity Framework Core进行增删改查操作的示例。 首先,创建一个空的ASP.NET Core 6.0 Web应用程序项目。 然后,安装以下NuGet包: Microsoft.EntityFra…...
HC32L110B6芯片测试
到货之后,直观上感觉的确很小,小包装盒里面还装了说明书。 下载器单独在一个盒里面,但是这个T-U2T没用上,还是用的STLINK。 开发之前先去网上找了一些别人遇到的坑,的确不少。 涉及的方面也是挺全的,供电、…...
关于我乱删注册表导致电脑没有声音这件事
之前因为想彻底删除迅雷,照着网上进入注册表一顿乱删,也忘记删了啥,反正把一顿xmp的文件,和搜索出来迅雷的全删了。结果迅雷确实没了,被带走的还有电脑的声音。 很离谱,就试过了所有方法都没用,…...
Linux 命令 su 和 sudo 的区别
之前一直对 su 和 sudo 这两个命令犯迷糊,最近专门搜了这方面的资料,总算是把两者的关系以及用法搞清楚了,这篇文章来系统总结一下。 1. 准备工作 因为本篇博客中涉及到用户切换,所以我需要提前准备好几个测试用户,方…...
微信小程序:Mobx的使用指南
简要 微信小程序中有时需要进行全局状态管理,这个时候就需要用到Mobx.下面我们来看一下在小程序中是如何使用Mobx的 安装 pnpm i mobx-miniprogram4.13.2 mobx-miniprogram-bindings1.2.1 或 npm i mobx-miniprogram4.13.2 mobx-miniprogram-bindings1.2.1 或 yarn…...
【Spring Boot】Spring Boot项目的创建和文件配置
目录 一、为什么要学Spring Boot 1、Spring Boot的优点 二、创建Spring Boot项目 1、创建项目之前的准备工作 2、创建Spring Boot项目 3、项目目录的介绍 4、安装Spring Boot快速添加依赖的插件 5、在项目中写一个helloworld 三、Spring Boot的配置文件 1、配置文件的…...
Spring Cloud 智慧工地源码(PC端+移动端)项目平台、监管平台、大数据平台
智慧工地源码 智慧工地云平台源码 智慧建筑源码 “智慧工地”是利用物联网、人工智能、云计算、大数据、移动互联网等新一代信息技术,彻底改变传统建筑施工现场参建各方现场管理的交互方式、工作方式和管理模式,实现对人、机、料、法、环的全方位实时监…...
通达OA SQL注入漏洞【CVE-2023-4165】
通达OA SQL注入漏洞【CVE-2023-4165】 一、产品简介二、漏洞概述三、影响范围四、复现环境POC小龙POC检测工具: 五、修复建议 免责声明:请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损…...
centos7 安装 docker 不能看菜鸟教程的 docker 安装,有坑
特别注意 不能看菜鸟教程的 docker 安装,有坑 如果机器不能直接上网,先配置 yum 代理 proxyhttp://172.16.0.11:8443 配置文件修改后即刻生效,再执行 yum install 等命令,就可以正常安装软件了。 参考 https://blog.csdn.net/c…...
♥ vue中$nextTick()
♥ vue中$nextTick() ① 语法 this.$nextTick(回调函数)② 作用 在下一次 DOM 更新结束后执行其指定的回调 使用时机----(比方Echarts地图的渲染) 当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行 ③ 案例: 实现…...
手把手教你用STM32F103的SPI2驱动FPGA(附Verilog从机代码)
STM32与FPGA的SPI通信实战:从硬件连接到代码调试全解析 在嵌入式系统开发中,处理器与可编程逻辑器件的协同工作变得越来越常见。STM32作为广泛使用的微控制器,与FPGA的高速通信是实现复杂系统功能的关键。本文将带你从零开始,完成…...
AI技术如何重塑气候预测与生态保护
1. NVIDIA GTC 2025:AI如何重塑气候与生态韧性技术版图 当全球平均气温持续突破历史记录,当极端天气事件开始以月为单位刷新灾害统计,我们正面临着一个前所未有的挑战:如何用技术手段为脆弱的生态系统构筑韧性防线。今年3月17-21日…...
2026届必备的五大降重复率助手横评
Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 在人工智能生成内容越来越普遍的情形下,把文本的“机器味”降下来成了提升内容质…...
FPGA异步FIFO读写位宽转换实战:从8bit到32bit的数据拼接与拆分(Vivado+Modelsim)
FPGA异步FIFO读写位宽转换实战:从8bit到32bit的数据拼接与拆分(VivadoModelsim) 在FPGA设计中,数据流处理经常面临跨时钟域和位宽不匹配的双重挑战。想象这样一个场景:传感器以8bit宽度持续输出数据,而DSP处…...
手把手教你用CarMaker 10.2和Matlab R2021a搭建联合仿真环境(附避坑指南)
从零开始构建CarMaker与Simulink联合仿真环境的完整指南 当车辆动力学仿真遇到控制系统设计,CarMaker与Simulink的联合仿真环境就像给工程师装上了涡轮增压器。这个强大的组合允许你在高度逼真的虚拟测试环境中验证控制算法,而无需等待物理原型。想象一下…...
Super Breadboard:8位复古计算原型开发板解析
1. Super Breadboard:为8位复古计算打造的全能原型开发板在硬件原型开发领域,面包板一直是电子爱好者和工程师快速验证电路设计的必备工具。但传统面包板存在供电不稳定、缺乏保护电路、信号管理混乱等痛点。Super Breadboard正是为解决这些问题而生的增…...
【深度解析】基于RK3568核心板的国产化工业方案:从1.8GHz Cortex-A55到1TOPS NPU的全栈优势
1. 全国产工业核心板的硬核实力 第一次拿到这块RK3568核心板的时候,我盯着那个只有信用卡三分之二大小的板子看了半天——就这么个小东西,居然塞进了4个Cortex-A55核心、1TOPS算力的NPU,还能硬解4K视频?更让我惊讶的是,…...
XXMI启动器:六款主流二次元游戏模组管理的统一解决方案
XXMI启动器:六款主流二次元游戏模组管理的统一解决方案 【免费下载链接】XXMI-Launcher Modding platform for GI, HSR, WW and ZZZ 项目地址: https://gitcode.com/gh_mirrors/xx/XXMI-Launcher 在当今游戏模组管理领域,XXMI启动器作为一款创新的…...
8大网盘直链解析神器:告别限速,体验全速下载的终极方案
8大网盘直链解析神器:告别限速,体验全速下载的终极方案 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 ,支持 百度网盘 / 阿里云盘 / 中国移动…...
推荐一些可以用于论文降重的软件:哪些可以同时降低查重与AIGC率?2026年爆款TOP5深度评测!
CSDN学术极客专栏 / 2026届毕业生抢救指南: 各位C站的科研同行、学弟学妹们,晚上好。临近毕业季,我的主页几乎被同一个问题刷爆:“博主,推荐一些可以用于论文降重的软件吧!我用常规工具降完了重,…...
