【数据结构】第五站:带头双向循环链表
目录
一、链表的八种结构
二、带头双向循环链表的实现
1.链表的定义
2.链表的接口定义
3.接口的具体实现
三、带头双向循环链表的完整代码
四、顺序表和链表的区别
一、链表的八种结构
我们已经知道链表可以有以下三种分法
![]()
![]()
而这三种结构又可以排列组合,形成八种结构
其中我们最常见的就是无头单向非循环链表和带头双向循环链表
1.无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了
二、带头双向循环链表的实现
1.链表的定义
对于这个链表,我们需要使用两个指针来控制,一个是前驱指针,一个是后继指针
typedef int LTDateType; typedef struct ListNode {struct ListNode* prev;struct ListNode* next;LTDateType data; }ListNode;
2.链表的接口定义
如下代码所示,是我们需要实现的接口
//链表的初始化 ListNode* ListCreat(); //链表的打印 void ListPrint(ListNode* phead); //链表的尾插 void ListPushBack(ListNode* phead, LTDateType x); //链表的头插 void ListPushFront(ListNode* phead, LTDateType x); //链表是否为空 bool ListEmpty(ListNode* phead); //链表的尾删 void ListPopBack(ListNode* phead); //链表的头删 void ListPopFront(ListNode* phead); //链表的查找 ListNode* ListFind(ListNode* phead, LTDateType x); //链表在pos前面的插入 void ListInsert(ListNode* pos, LTDateType x); //链表在pos位置处的删除 void ListErase(ListNode* pos); //链表的销毁 void ListDestroy(ListNode* phead);
3.接口的具体实现
1.链表的初始化
ListNode* BuyListNode(LTDateType x) {ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));if (newnode == NULL){perror("malloc");return NULL;}newnode->data = x;newnode->next = NULL;newnode->prev = NULL;return newnode; } //链表的初始化 ListNode* ListCreat() {ListNode* phead = BuyListNode(-1);phead->next = phead;phead->prev = phead;return phead; }
如上代码所示,对于这个初始化,他与单链表不同,单链表其实是没有必要去专门做一个初始化函数的。但同时我们需要传二级指针来解决一些问题
对于双向带头循环链表,我们就有必要设置一个初始化函数了。
我们有两种思路去构建这个函数
一种是在主函数中直接定义一个指针,然后通过传这个指针的地址去构建。但是这样我们就需要传二级指针了。
另外一种是我们可以利用返回值,这样我们就直接避免了传参数了。
2.链表的打印
//链表的打印 void ListPrint(ListNode* phead) {assert(phead);ListNode* cur = phead->next;printf("<=head=>");while (cur != phead){printf("%d<=>", cur->data);cur = cur->next;}printf("\n"); }
如上代码所示,链表的打印与单链表的打印是一样的,没有什么太大的区别,我们可以利用printf来更加形象的表达双向循环的意思
3.链表的尾插
//链表的尾插 void ListPushBack(ListNode* phead, LTDateType x) {assert(phead);ListNode* newnode = BuyListNode(x);ListNode* tail = phead->prev;tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode; }
对于尾插就比单链表简单多了,因为有了头节点,我们就不需要处理是否为空的状态了
4.链表的头插
//链表的头插 void ListPushFront(ListNode* phead, LTDateType x) {assert(phead);ListNode* newnode = BuyListNode(x);ListNode* first = phead->next;phead->next = newnode;newnode->prev = phead;newnode->next = first;first->prev = newnode; }
头插和尾插基本上是非常类似的,这也得益于这种链表结构基本是无死角的
5.链表的头删尾删以及是否为空
//链表是否为空 bool ListEmpty(ListNode* phead) {assert(phead);return phead->next == phead; } //链表的尾删 void ListPopBack(ListNode* phead) {assert(phead);assert(!ListEmpty(phead));ListNode* tail = phead->prev;ListNode* tailPrev = tail->prev;phead->prev = tailPrev;tailPrev->next = phead;free(tail);tail = NULL; } //链表的头删 void ListPopFront(ListNode* phead) {assert(phead);assert(!ListEmpty(phead));ListNode* first = phead->next;ListNode* firstNext = first->next;phead->next = firstNext;firstNext->prev = phead;free(first);first = NULL; }
对于头删和尾删其实也是一致的思路,我们只需要改变结点即可,值得注意的是,我们需要判断链表是否为空, 如果为空肯定不可以删除
6.链表的查找
//链表的查找 ListNode* ListFind(ListNode* phead,LTDateType x) {assert(phead);assert(!ListEmpty(phead));ListNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL; }
这个也是比较简单的,他的思路与单链表的是一样的
7.链表在pos位置之前插入
//链表在pos前面的插入 void ListInsert(ListNode* pos, LTDateType x) {assert(pos);ListNode* newnode = BuyListNode(x);ListNode* prev = pos->prev;prev->next = newnode;newnode->prev = prev;newnode->next = pos;pos->prev = newnode; }
这个的话,我们更能体会到他相对于单链表的优势了。我们只需要pos位置即可。然后思路和头插尾插是一致的
8.链表在pos位置的删除
//链表在pos位置处的删除 void ListErase(ListNode* pos) {assert(pos);ListNode* prev = pos->prev;ListNode* next = pos->next;prev->next = next;next->prev = prev;free(pos);pos = NULL; }
对于删除,也是与头删尾删思路一致的
9.链表的销毁
//链表的销毁 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; }
对于链表的销毁思路与单链表也是一致的,只需要循环遍历即可
三、带头双向循环链表的完整代码
List.h
#pragma once #include<stdio.h> #include<malloc.h> #include<assert.h> #include<stdbool.h> typedef int LTDateType; typedef struct ListNode {struct ListNode* prev;struct ListNode* next;LTDateType data; }ListNode;//链表的初始化 ListNode* ListCreat(); //链表的打印 void ListPrint(ListNode* phead); //链表的尾插 void ListPushBack(ListNode* phead, LTDateType x); //链表的头插 void ListPushFront(ListNode* phead, LTDateType x); //链表是否为空 bool ListEmpty(ListNode* phead); //链表的尾删 void ListPopBack(ListNode* phead); //链表的头删 void ListPopFront(ListNode* phead); //链表的查找 ListNode* ListFind(ListNode* phead, LTDateType x); //链表在pos前面的插入 void ListInsert(ListNode* pos, LTDateType x); //链表在pos位置处的删除 void ListErase(ListNode* pos); //链表的销毁 void ListDestroy(ListNode* phead);
List.c
#define _CRT_SECURE_NO_WARNINGS 1 #include"List.h" ListNode* BuyListNode(LTDateType x) {ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));if (newnode == NULL){perror("malloc");return NULL;}newnode->data = x;newnode->next = NULL;newnode->prev = NULL;return newnode; } //链表的初始化 ListNode* ListCreat() {ListNode* phead = BuyListNode(-1);phead->next = phead;phead->prev = phead;return phead; } //链表的打印 void ListPrint(ListNode* phead) {assert(phead);ListNode* cur = phead->next;printf("<=head=>");while (cur != phead){printf("%d<=>", cur->data);cur = cur->next;}printf("\n"); } //链表的尾插 void ListPushBack(ListNode* phead, LTDateType x) {assert(phead);ListNode* newnode = BuyListNode(x);ListNode* tail = phead->prev;tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode; } //链表的头插 void ListPushFront(ListNode* phead, LTDateType x) {assert(phead);ListNode* newnode = BuyListNode(x);ListNode* first = phead->next;phead->next = newnode;newnode->prev = phead;newnode->next = first;first->prev = newnode; } //链表是否为空 bool ListEmpty(ListNode* phead) {assert(phead);return phead->next == phead; } //链表的尾删 void ListPopBack(ListNode* phead) {assert(phead);assert(!ListEmpty(phead));ListNode* tail = phead->prev;ListNode* tailPrev = tail->prev;phead->prev = tailPrev;tailPrev->next = phead;free(tail);tail = NULL; } //链表的头删 void ListPopFront(ListNode* phead) {assert(phead);assert(!ListEmpty(phead));ListNode* first = phead->next;ListNode* firstNext = first->next;phead->next = firstNext;firstNext->prev = phead;free(first);first = NULL; } //链表的查找 ListNode* ListFind(ListNode* phead,LTDateType x) {assert(phead);assert(!ListEmpty(phead));ListNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL; } //链表在pos前面的插入 void ListInsert(ListNode* pos, LTDateType x) {assert(pos);ListNode* newnode = BuyListNode(x);ListNode* prev = pos->prev;prev->next = newnode;newnode->prev = prev;newnode->next = pos;pos->prev = newnode; } //链表在pos位置处的删除 void ListErase(ListNode* pos) {assert(pos);ListNode* prev = pos->prev;ListNode* next = pos->next;prev->next = next;next->prev = prev;free(pos);pos = NULL; } //链表的销毁 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; }
Test.c
#define _CRT_SECURE_NO_WARNINGS 1#include"List.h"void TestList1() {ListNode* plist = ListCreat();ListPushBack(plist, 1);ListPushBack(plist, 2);ListPushBack(plist, 3);ListPushBack(plist, 4);ListPushBack(plist, 5);ListPrint(plist);ListPushFront(plist, 6);ListPushFront(plist, 7);ListPushFront(plist, 8);ListPushFront(plist, 9);ListPushFront(plist, 10);ListPrint(plist);ListPopBack(plist);ListPopBack(plist);ListPopBack(plist);ListPopBack(plist);ListPrint(plist);ListPopFront(plist);ListPopFront(plist);ListPopFront(plist);ListPopFront(plist);ListPrint(plist);} void TestList2() {ListNode* plist = ListCreat();ListPushBack(plist, 1);ListPushBack(plist, 2);ListPushBack(plist, 3);ListPushBack(plist, 4);ListPushBack(plist, 5);ListPrint(plist);ListNode* pos = ListFind(plist, 3);(pos->data) *= 10;ListPrint(plist);ListInsert(pos, 20);ListPrint(plist);ListErase(pos);ListPrint(plist);} int main() {//TestList1();TestList2();return 0; }
四、顺序表和链表的区别
不同点 顺序表 链表 存储空间上 物理上一定连续 逻辑上连续,但物理上不一定连续 随机访问 支持,O(1) 不支持,O(N) 任意位置插入或删除元素 可能需要搬移元素,效率低,O(N) 只需要改变指针指向 插入 动态顺序表,空间不够时需要扩容 没有容量的概念 应用场景 元素高效存储+频繁访问 任意位置插入和删除频繁 缓存利用率 高 低
如上表所示,是顺序表和链表的区别
在这里我们需要注意缓存利用率这个东西。
对于我们的顺序表/链表,想要实现他的数据访问、遍历、修改等
这些数据是由cpu来进行访问的。
也就是这些访问的指令都是cpu执行的。从而访问打印等操作。这时候cpu就需要去访问内存
而cpu其实是不能直接访问内存的,这是因为cpu太快了,内存太慢了
为了解决这个问题,就有了三级缓存,即cpu高速缓存这个东西。
也就是说cpu会先访问缓存,最高级的缓存再次缓存其实就是命中。
在计算机中有一个局部性原理,在访问某个位置的数据的时候,大概率要访问后面的数据。因此他就会将这个数据后面的数据也顺便加载进去。
这时候由于顺序表是连续的,后面的都直接被命中了。所以他的缓存利用率高
而链表他访问某个数据的时候也会加载一长段,但是后面的不一定会用。所以就会浪费,也会导致缓存污染。所以缓存利用率低。也就是他不仅没有起到正向作用,反而起到了反向作用。
本节内容到此位置
如果对你有帮助的话,不要忘记点赞加收藏哦!!!
相关文章:

【数据结构】第五站:带头双向循环链表
目录 一、链表的八种结构 二、带头双向循环链表的实现 1.链表的定义 2.链表的接口定义 3.接口的具体实现 三、带头双向循环链表的完整代码 四、顺序表和链表的区别 一、链表的八种结构 我们已经知道链表可以有以下三种分法 而这三种结构又可以排列组合,形成八…...

Springboot生成二维码
Springboot生成二维码整合 我们使用两种方式,去生成二维码,但是其实,二维码的生成基础,都是zxing包,这是Google开源的一个包,第一种是使用原始的zxing方式去实现,第二种是使用hutool来实现&…...

“独裁者”何小鹏,再造小鹏汽车
文丨智能相对论 作者丨沈浪 如果没有何小鹏,小鹏汽车将失去灵魂。 2014年,夏珩、何涛等人在广州组建小鹏汽车(当时还叫“橙子汽车”),何小鹏还只是股权投资人。 夏珩、何涛原任职于广汽,负责新能源汽车…...

数据结构 | 泛型 | 擦除机制| 泛型的上界
目录 编辑 1.泛型 1.1Object类引出泛型概念 2.泛型语法 2.1泛型编写代码 3.泛型的机制 3.1擦除机制 4.泛型的上界 4.1泛型上界的语法 4.2泛型上界的使用 5.泛型方法 5.1泛型方法语法 5.2泛型方法的使用 1.泛型 一般的类和方法中,只能使用具体的代码…...

C++拷贝构造函数(复制构造函数)详解
拷贝和复制是一个意思,对应的英文单词都是copy。对于计算机来说,拷贝是指用一份原有的、已经存在的数据创建出一份新的数据,最终的结果是多了一份相同的数据。例如,将 Word 文档拷贝到U盘去复印店打印,将 D 盘的图片拷…...

python学习——多线程
python学习——多线程概念python中线程的开发线程的启动线程的退出和传参threading的属性和方法threading实例的属性和方法多线程daemon线程和non-demone线程daemon线程的应用场景线程的jointhreading.local类线程的延迟执行:Timer线程同步Event 事件Lock ——锁加锁…...

SAP 系统中过账码or记账码
SAP中过账码和记账码是指同一个事物。 在实际业务中,记账码就是只有“借”和“贷”, 而SAP中Posting Code肩负着更多的任务: 1)界定科目类型, 2)借贷方向, 3)凭证输入时画面上的字…...

【FreeRTOS(一)】FreeRTOS新手入门——初识FreeRTOS
初识FreeRTOS一、实时操作系统概述1、概念2、RTOS的必要性3、RTOS与裸机的区别4、FreeRTOS的特点二、FreeRTOS的架构三、FreeRTOS的代码架构一、实时操作系统概述 1、概念 RTOS:根据各个任务的要求,进行资源(包括存储器、外设等)…...

Python中 __init__的通俗解释是什么?
__init__是Python中的一个特殊方法,用于在创建对象时初始化对象的属性。通俗来讲,它就像是一个构造函数,当我们创建一个类的实例时,__init__方法会被自动调用,用于初始化对象的属性。 举个例子,如果我们定义…...

网友真实面试总结出的自动化测试面试题库
目录 常规问题 手工测试部 自动化测试 自动化测试面试题2:selenium篇 常规问题 1、如何快速深入的了解移动互联网领域的应用 (答案:看http协议 restful api知识 json加1分) 2、对xx应用自己会花多久可以在业务上从入门到精通&…...

2023 年最佳 C++ IDE
文章目录前言1. Visual Studio2. Code::Blocks3. CLion4. Eclipse CDT(C/C 开发工具)5. CodeLite6. Apache NetBeans7. Qt Creator8. Dev C9. C Builder10. Xcode11. GNAT Programming Studio12. Kite总结前言 要跟踪极佳 IDE(集成开发环境&…...

在Ubuntu上使用VSCode编译MySQL Connector/C连接库
首先下载并解压MySQL Connector/C源码,然后执行以下步骤: 1、安装MySQL Connector/C依赖:在终端中输入以下命令来安装MySQL Connector/C的依赖项: sudo apt-get install build-essential cmake 2、下载并解压MySQL Connector/C源…...

单声道数字音频放大器AD87589
AD87589是一种集成音频系统解决方案,嵌入数字音频处理、功率级放大器和立体声2Vrms线路驱动器。 AD87589具有可编程转换速率控制的输出缓冲器,可直接驱动一个(单声道)或两个(立体声)扬声器。此外࿰…...

网络的UDP协议和TCP协议
协议:数据在网络中的传输规则,常见的协议有 UDP协议和TCP协议 协议:计算机网络中,连接和通信的规则被称为网络通信协议 UDP协议:用户数据报协议,是面向无连接通信协议,速度快,有大小…...

【JaveEE】多线程之阻塞队列(BlockingQueue)
目录 1.了解阻塞队列 2.生产者消费者模型又是什么? 2.1生产者消费者模型的优点 2.1.1降低服务器与服务器之间耦合度 2.1.2“削峰填谷”平衡消费者和生产的处理能力 3.标准库中的阻塞队列(BlockingQueue) 3.1基于标准库(Bloc…...

分布式ELK日志监控系统环境搭建
文章目录1.1为什么需要监控项目日志1.2ELK日志监控系统介绍1.3ELK的工作流程1.4ELK环境搭建1.4.1Elasticsearch的安装1.4.2Kibana的安装1.4.3Logstash的安装1.4.4数据源配置1.4.5日志监测测试1.4.6日志数据可视化展示1.1为什么需要监控项目日志 项目日志是记录项目运行过程中产…...

【数据结构刷题集】链表经典习题
😽PREFACE🎁欢迎各位→点赞👍 收藏⭐ 评论📝📢系列专栏:数据结构刷题集🔊本专栏涉及到题目是数据结构专栏的补充与应用,只更新相关题目,旨在帮助提高代码熟练度&#x…...

JavaSE(3.27) 异常
学习不要眼高手低,学习是一点点积累的。即使你现在很菜,坚持学一个学期不会差的!只要花时间学习,每天都是进步的,这些进步可能你现在看不到,但是不要小瞧了积累效应,30天,60天&#…...

【看门狗】我说的是定时器不是狗啊
单片机在运行中死机了,你或许只能按2下电源键(重启)或1下复位键。 这里简单说一下重启和复位: 从RESET引脚复位,只有MCU复位。而外设看情况,有的可能会有MCU同步复位或者重新初始化。也有可能一些保持复位…...

24万字智慧城市顶层设计及智慧应用解决方案
本资料来源公开网络,仅供个人学习,请勿商用,如有侵权请联系删除。部分资料内容: 4.8 机房消防系统 4.8.1消防系统概况 根据本工程机房消防系统的特殊要求,整个消防系统由火灾报警系统、消防联动系统和气体灭火系统三部…...

跨境电商卖家工具——跨境卫士内容介绍
一、简介 跨境卫士是一款集合多种跨境电商工具的综合软件,由知名跨境电商服务商跨境通开发。跨境卫士可以帮助卖家完成海外物流管理、订单处理、报关报税、市场营销等多项任务,同时还提供数据分析、客户服务、运营管理等一系列支持功能,方便卖…...

Redis 常用基本命令
关于 redis 的常用基本命令 目录 关于 redis 的常用基本命令 1. 关于 key 的操作 2. HyperLogLog 求近似基数 3. 排序相关命令 4. Limit 限制查询 1. 关于 key 的操作 判断某个 key 是否存在 # 格式: exists key exists name# 存在name 返回1 # 不存在name 返回0 查找或…...

【Leetcode】队列的性质与应用
文章目录225. 用队列实现栈示例:提示:分析:题解:622. 设计循环队列示例:提示:分析:题解:225. 用队列实现栈 请你仅使用两个队列实现一个后入先出(LIFO)的栈&…...

开启新航路,拓尔思发力AIGC市场 | 爱分析调研
2022年,随着AI聊天机器人GhatGPT在世界范围内持续火爆,极具创意、表现力、个性化且能快速迭代的AIGC技术成功破圈,成为全民讨论热点。 AIGC是指在确定主题下,由算法模型自动生成内容,包括单模态内容如文本、图像、音频…...

RK3568平台开发系列讲解(调试篇)Linux 内核的日志打印
🚀返回专栏总目录 文章目录 一、dmseg 命令二、查看 kmsg 文件三、调整内核打印等级沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇将 Linux 内核的日志打印进行梳理。 一、dmseg 命令 在终端使用 dmseg 命令可以获取内核打印信息,该命令的具体使用方法如下所…...

hadoop之MapReduce框架原理
目录 MapReduce框架的简单运行机制: Mapper阶段: InputFormat数据输入: 切片与MapTask并行度决定机制: job提交过程源码解析: 切片逻辑: 1)FileInputFormat实现类 进行虚拟存储 &#x…...

JavaEE简单示例——SpringMVC的简单数据绑定
简单介绍: 在前面我们介绍过如何将我们自己创建的类变成一个servlet来处理用户发送的请求,但是在大多数的时候,我们在请求 的时候会携带一些参数,而我们现在就开始介绍我们如何在Java类中获取我们前端请求中携带的参数。首先&…...

耗时的同步请求自动转异步请求
耗时的同步请求自动转异步请求问题描述问题处理代码实现问题描述 现在在项目中碰到一个情况,导出数据到excel,在数据量比较下的时候直接下载,在数据量比较大时保存到服务的文件列表,后续再供用户下载。 也就是需要避免前端因后端…...

React常见的hook
目录 useState useEffect useRef useContext useCallback useMemo useState const [初始值,修改值的方法] useState( 初始值 ) 我们用useState定义一个初始值,可以打印看一下结果 console.log(useState(10)) // [10, ƒ] 结果是一个数组…...

Oracle集群管理ASM-扩容磁盘组报错ora-15137
1 内容描述 今日对19c集群磁盘组进行扩容, [rootdb1 ~]# oracleasm createdisk DATA7 /dev/sdm1 Writing disk header: done Instantiating disk: done [rootdb1 ~]# oracleasm createdisk DATA8 /dev/sdn1 Writing disk header: done Instantiating disk: done 使…...