【编织时空四:探究顺序表与链表的数据之旅】

news2024/11/19 13:38:51

本章重点

  • 链表的分类

  • 带头双向循环链表接口实现

  • 顺序表和链表的区别

  • 缓存利用率参考存储体系结构 以及 局部原理性。

一、链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

1. 单向或者双向

2. 带头或者不带头

3. 循环或者非循环 

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带 来很多优势,实现反而简单了,后面我们代码实现了就知道了。

二、带头双向循环链表接口实现

1.申请结点:struct ListNode* BuyLTNode(LTDataType x)

动态申请结点,函数返回的是一个指针类型,用malloc开辟一个LTNode大小的空间,并用node指向这个空间,再判断是否为空,如为空就perror,显示错误信息。反之则把要存的数据x存到newnode指向的空间里面,把指针置为空。

// 申请结点
struct ListNode* BuyLTNode(LTDataType x)
{
	struct ListNode* node = (struct ListNode*)malloc(sizeof(struct ListNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	node->data = x;
	node->prev = node;
	node->next = node;

	return node;
}

2.初始化:struct ListNode* LTInit()

// 初始化
void LTInit(struct ListNode* phead)
{
	phead = BuyLTNode(-1);
	phead->prev = phead;
	phead->next = phead;
}

我们首先看看这个初始化有什么问题嘛?

phead为空指针,说明我们的初始化没有效果,这是因为phead是函数里面的形参,出了作用域就销毁,plsit仍然是空指针,形参的改变不能影响实参,但是我们可以通过返回phead的地址解决问题。

单链表开始是没有节点的,可以定义一个指向空指针的结点指针,但是此链表不同,需要在初始化函数中创建个头结点,它不用存储有效数据。因为链表是循环的,在最开始需要让头结点的next和pre指向头结点自己。

因为其他函数也不需要用二级指针(因为头结点指针是不会变的,变的是next和pre,改变的是结构体,只需要用结构体针即可,也就是一级指针)为了保持一致此函数也不用二级指针,把返回类型设置为结构体指针类型。

// 初始化 - 改变实参plsit
struct ListNode* LTInit()
{
	struct ListNode* phead = BuyLTNode(-1);
	phead->prev = phead;
	phead->next = phead;

	return phead;
}

3.尾插:void LTPushBack(struct ListNode* phead, LTDataType x)

尾插首先要找到尾结点,再将要尾插的结点与尾结点和带头结点链接,由于是带头结点,所以此处不需要关注头结点为空的问题。

// 尾插
void LTPushBack(struct ListNode* phead, LTDataType x)
{
	assert(phead);

	struct ListNode* tail = phead->prev;
	struct ListNode* newnode = BuyLTNode(x);

	newnode->prev = tail;
	tail->next = newnode;

	newnode->next = phead;
	phead->prev = newnode;

}

4.尾删:void LTPopBack(struct ListNode* phead)

尾删只需要找到尾结点的前驱结点,再把带头结点和前驱结点链接,释放尾结点就完成了尾删。

不过这里需要处理一下只有带头结点的删除,此时真正的链表为空,此时就不能删除了。

// 尾删
void LTPopBack(struct ListNode* phead)
{
	assert(phead);

	assert(phead->next != phead);//链表为空

	struct ListNode* tail = phead->prev;
	struct ListNode* tailPrev = tail->prev;
	
	free(tail);
	tailPrev->next = phead;
	phead->next = tailPrev;
	
}

5.头插:void LTPushFront(struct ListNode* phead, LTDataType x)

头插需要注意顺序,如果先让phead和newnode链接,那么就找不到phead结点的后续结点,这样就无法让newnode和phead结点的后续结点链接。

// 头插
void LTPushFront(struct ListNode* phead, LTDataType x)
{
	assert(phead);
	
	struct ListNode* newnode = BuyLTNode(x);

	//顺序不可颠倒
	newnode->next = phead->next;
	phead->next->prev = newnode;

	phead->next = newnode;
	newnode->prev = phead;
}

6.头删:void LTPopFront(struct ListNode* phead)

// 头删
void LTPopFront(struct ListNode* phead)
{
	assert(phead);

	assert(phead->next != phead);//链表为空

	struct ListNode* first = phead->next;
	struct ListNode* second = first->next;

	free(first);
	phead->next = second;
	second->prev = phead;
}

7.链表长度:int LTSize(struct ListNode* phead)

        求链表长度,先把头结点下一个结点存到cur中,再用while循环遍历终止条件是cur等于头结点,用size++记录长度,并更新cur,最后返回size,32位机器下是无符号整型size_t。
( 此类型可以提高代码的可移植性,有效性,可读性。此链表长度可能是字符型或者数组整型,他可以提供一种可移植方法来声明与系统中可寻址的内存区域一致的长度,而且被设计的足够大,能表示内存中任意对象的大小)

        注意这里求链表长度不需要求带头结点,我们有时也经常看到由于带头结点的数据没有使用,就有的书上会把该数据存储上链表的长度size=phead->data,然后插入数据phead->data++,删除phead->--,但是这有个限制,这种写法只适合int类型,如果我们写出char类型的数据,存储几个数据char就保存不了,char类型的phead->data一直++最后就会溢出,所以不建议这种写法。

// 链表长度
int LTSize(struct ListNode* phead)
{
	assert(phead);
	int size = 0;
	struct ListNode* cur = phead->next;

	while (cur != phead)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

8.在pos的前面进行插入:void LTInsert(struct ListNode* pos, LTDataType x)

断言pos,不能为空,插入数据先申请一结点放到定义的newnode指针变量中,为了不用考虑插入顺序,先把pos前面的存到posPrev中,然后就可以随意链接:
posPrev指向新节点,新节点前驱指针指向posPrev,新节点指向pos,pos前驱指针指向新节点。

// 在pos的前面进行插入
void LTInsert(struct ListNode* pos, LTDataType x)
{
	assert(pos);

	struct ListNode* posPrev = pos->prev;
	struct ListNode* newnode = BuyLTNode(x);

	posPrev->next = newnode;
	newnode->prev = posPrev;

	newnode->next = pos;
	pos->prev = newnode;
}

9.删除pos位置的结点:void LTErase(struct ListNode* pos)

删除把pos位置之前的结点直接指向pos的下一个结点,把pos下一个结点的前驱指针指向pos之前的结点。

// 删除pos位置的结点
void LTErase(struct ListNode* pos)
{
	assert(pos);

	struct ListNode* posPrev = pos->prev;
	struct ListNode* posNext = pos->next;

	free(pos);

	posPrev->next = posNext;
	posNext->prev = posPrev;
}

 

10.双向链表查找:struct ListNode* ListFind(struct ListNode* phead, LTDataType x)

查找把头结点下一个结点存到cur,然后用while循环遍历,终止条件是cur等于头结点指针,如果cur等于x,直接返回cur指针,再更新cur,最后遍历完返回NULL,表示没有该数据。

// 双向链表查找
struct ListNode* ListFind(struct ListNode* phead, LTDataType x)
{
	assert(phead);
	struct ListNode* cur = phead->next;

	while (cur != phead)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

11.销毁:void LTDestroy(struct ListNode* phead)

释放链表从头开始释放,把头结点下一个结点存到cur中,再用用while循环,终止条件是cur不等于头指针,在里面把cur下一个指针存到next中,释放掉cur,再把next更新为cur。最后头结点也是申请的,也得释放。

这里需要注意,由于形参的改变不会影响实参,我们在函数内部将phead置空是无意义的,我们需要在函数外面调用LTDestroy函数后,需要手动将phead置空。

// 销毁
void LTDestroy(struct ListNode* phead)
{
	assert(phead);
	
	struct ListNode* cur = phead->next;

	while (cur != phead)
	{
		struct ListNode* next = cur->next;
		free(cur);

		cur = next;
	}
	free(phead);
}

12.打印:void LTPrint(struct ListNode* phead)

打印链表,先断言phead,它不能为空,再把头结点下个地址存到cur中,用while循环去遍历,终止条件是等于头指针停止,因为他是循环的,并更新cur。

// 打印
void LTPrint(struct ListNode* phead)
{
	assert(phead);
	printf("phead<=>");
	struct ListNode* cur = phead->next;

	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
}

三、顺序表和链表的区别

不同点顺序表链表
存储空间上物理上一定连续逻辑上连续,但物理上不一定 连续
随机访问支持O(1)不支持:O(N)
任意位置插入或者删除 元素可能需要搬移元素,效率低 O(N)只需修改指针指向
插入动态顺序表,空间不够时需要 扩容没有容量的概念
应用场景元素高效存储+频繁访问任意位置插入和删除频繁
缓存利用率

四、缓存利用率参考存储体系结构 以及 局部原理性。

1.存储体系结构

  1. 寄存器(Registers):寄存器是CPU内部的最快速存储,用于存储最常用的数据和指令。它们在执行指令时起着重要作用,速度非常快。

  2. 高速缓存(Cache):高速缓存是位于中央处理器(CPU)和主存储器之间的一层快速存储,用于临时存放经常访问的数据和指令。它可以分为多个层次,如L1、L2和L3缓存,随着级别的升高,容量逐渐增大,但速度逐渐降低。

  3. 主存储器(Main Memory):也称为RAM(Random Access Memory),主存储器是计算机中用于存放运行中程序和数据的地方。它是CPU能够直接访问的存储设备,速度比较快,但容量通常相对有限。

  4. 辅助存储器(Secondary Storage):这包括各种长期存储设备,如硬盘驱动器、固态硬盘、光盘、磁带等。这些设备的容量通常很大,但速度较慢,用于持久性存储和备份。

  5. 虚拟内存(Virtual Memory):虚拟内存是一种在主存和辅助存储之间创建的抽象层,允许程序使用比主存更大的地址空间。操作系统可以根据需要将数据从主存移到辅助存储,以优化内存使用。

2.局部性原理

        存储体系结构的局部性原理是指在计算机程序执行过程中,访问内存的模式往往呈现出一定的规律性,即数据和指令的访问并不是完全随机的,而是倾向于集中在某些特定的内存区域或者特定的数据块上。这个原理分为两种类型:时间局部性和空间局部性。

  1. 时间局部性(Temporal Locality):时间局部性指的是一个被访问过的内存位置在未来的一段时间内很可能会再次被访问。这意味着程序在不久的将来可能会多次使用相同的数据或指令。时间局部性的实现依赖于高速缓存,因为缓存可以暂时存储最近使用过的数据,从而在未来的访问中加速存取。

  2. 空间局部性(Spatial Locality):空间局部性指的是在访问某个内存位置时,附近的内存位置也很可能会被访问。这种情况在程序中存在连续存储、数组访问等情况下特别明显。空间局部性的实现同样依赖于高速缓存,因为缓存可以在一个较小的区域内存储多个相邻的数据块。

        局部性原理对计算机性能具有重要意义。通过充分利用局部性,计算机系统可以更有效地使用高速缓存,减少主存访问次数,从而提高数据访问的速度和效率。这对于减少存储层次之间的数据传输时间以及提高程序的整体性能至关重要。

        举例来说,如果一个循环中多次使用相同的数组元素,由于时间局部性,这些数组元素会被缓存在高速缓存中,从而避免了多次访问主存。同样,如果一个算法中需要顺序访问数组的各个元素,由于空间局部性,缓存中可能会存储这些相邻的数据块,从而加速了后续的访问。

3.为什么顺序表的效率比链表效率高

  1. 顺序表:顺序表是一种将元素顺序存储在一块连续的内存区域中的数据结构。由于顺序表的元素在内存中是相邻存储的,因此它充分利用了空间局部性。当访问一个元素时,由于它的相邻元素也在内存中,这些相邻元素很可能也会被加载到高速缓存中,从而减少了主存访问的次数。此外,由于顺序表的元素是连续存储的,通过数组索引可以直接计算出元素的地址,避免了链表节点的跳转操作。

  2. 链表:链表是一种通过节点和指针连接元素的数据结构,它的元素在内存中不一定是连续存储的。在链表中,每个节点都包含了数据以及指向下一个节点的指针。这种非连续的存储方式可能导致空间局部性较差,因为在访问一个节点时,并不保证它的相邻节点会被加载到高速缓存中。这会导致更频繁的主存访问,从而降低了效率。

        综上所述,顺序表相比链表具有更好的空间局部性。它的元素连续存储,使得访问一个元素时,附近的元素也可能在缓存中,从而减少了主存访问的需求,提高了数据访问的速度和效率。而链表的节点之间不一定相邻存储,导致空间局部性不如顺序表,因此链表在访问数据时可能需要更多的主存访问操作,从而效率相对较低。

        需要注意的是,对于插入、删除等操作,链表在某些情况下可能更加灵活,因为它们不需要移动大量的数据,而顺序表在插入、删除操作时可能需要进行元素的移动,可能会带来一些开销。所以,在选择使用顺序表还是链表时,需要根据具体的应用场景和操作需求综合考虑。

本章结束啦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/898362.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【论文解读】Observation-Centric SORT:Rethinking SORT for Robust Multi-Object Tracking

一.介绍 1.1 之前卡尔曼方法存在的问题&#xff1a; 1.长时间的运动的线性估计可能是非常不准确的。2.当没有可用于更新卡尔曼滤波器参数的测量时&#xff0c;标准惯例是信任先验状态估计进行后验更新&#xff0c;这导致了在一段时间内错误的积累。 1.2 基于假设 假设跟踪目…

ARM体系结构学习笔记:位操作和灵活的2nd操作数

位操作 移位运算 数据传输指令 灵活的2nd操作数 Set a bit a | (1 << 5)Clear a bit a & ~(1 << 5)Toggling a bt a ^ 1<<5

IDEA开发项目时一直出现http404错误的解决方法

系列文章目录 安装cv2库时出现错误的一般解决方法_cv2库安装失败 SQL&#xff1e; conn sys/root as sysdbaERROR:ORA-12560: TNS: 协议适配器错误的解决方案 虚拟机启动时出现“已启用侧通道缓解”的解决方法 Hypervisor launch failed&#xff1b; Processor does not pr…

数据的绘画工场:Python绘图库Pyecharts,打造引人入胜的可视化效果

欢迎阅读本篇文章&#xff0c;本文将带您从零开始&#xff0c;逐步掌握使用Pyecharts库进行数据可视化的技能。Pyecharts是一个基于Echarts的Python可视化库&#xff0c;能够轻松创建各种交互式图表和地图&#xff0c;无论您是数据分析新手还是有经验的开发者&#xff0c;本文都…

【计算机视觉】相机基本知识(还在更新)

1.面阵工业相机与线阵工业相机 1.1 基本概念区别 面阵相机则主要采用的连续的、面状扫描光线来实现产品的检测&#xff1b; 线阵相机即利用单束扫描光来进行物体扫描的工作的。 1.2 优缺点 &#xff08;1&#xff09;面阵CCD工业相机&#xff1a; 优点&#xff1a;应用面…

ZooKeeper集群服务器启动

在本文中&#xff0c;我们将对集群版ZooKeeper服务器的启动过程做详细讲解。集群和单机ZooKeeper服务器的启动过程在很多地方都是一致的&#xff0c;因此本节只会对有差异的地方展开进行讲解。下图所示是集群版ZooKeeper服务器的启动流程图。 预启动 预启动的步骤如下。 (1)统…

财报解读:上半年业绩实现增长,药师帮业务飞轮已经开始旋转?

今年6月底登陆港股的药师帮&#xff0c;近日发布了上市后的首份财务报告。 财报显示&#xff0c;2023年上半年&#xff0c;药师帮实现营收增长、经调整后净利润转正的成果&#xff0c;再次验证了二级市场对于其发展潜力的看好——6月底上市以来&#xff0c;药师帮股价涨幅接近…

Mybatis的学习笔记(IDEA快捷键,参数占位符,转义符)

一、IDEA快捷键&#xff1a; IDEA多行注释&#xff1a;ctrlShift/ 单行注释&#xff1a;ctrl/ 导入包&#xff0c;自动修正代码&#xff1a;altenter 自动生成代码&#xff1a;altinsert 二、Mybatis重要知识点&#xff1a; 2.1 参数占位符 一共分为2种&#xff1a;#{}和…

idea2023 springboot+mybatis+jsp 初学单表增删改查

创建项目 因为2.7.14使用量较少&#xff0c;特更改spring-boot为2.7.5版本 配置端口号 打开Sm01Application类&#xff0c;右键运行启动项目&#xff0c;或者按照如下箭头启动 启动后&#xff0c;控制台提示如下信息表示成功 此刻在浏览器中输入&#xff1a;http://lo…

盘点市面上的ipad协议对比

友情链接 geweapi.com 点击即可访问! Web网页端&#xff1a;2017年后不再支持新号登录&#xff0c;仅支持老号&#xff0c;并且掉线严重&#xff0c;功能缺失严重。 Xposed技术&#xff1a;在2019年6月份&#xff0c;微信官方在行业重点打击Xposed&#xff0c;自此行业内一片…

企望制造ERP系统 RCE漏洞[2023-HW]

企望制造ERP系统 RCE漏洞 一、 产品简介二、 漏洞概述三、 复现环境四、 漏洞复现小龙POC检测 五、 修复建议 免责声明&#xff1a;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;…

IO day 4

1、使用两个进程完成两个文件的拷贝&#xff0c;父进程拷贝前一半内容&#xff0c;子进程拷贝后一半内容&#xff0c;并且父进程要阻塞回收子进程资源 #include <myhead.h>int main(int argc, const char *argv[]) {char a[1] {0};pid_t pid;pid fork();//创建一个子进…

马哈鱼数据血缘工具背后的项目: gsp_demo_java 项目简单介绍与使用

0.背景 马哈鱼数据血缘工具(https://www.sqlflow.cn/)是SQLflow工具的中文译名,实际就是sqlflow. 对于SQL flow来说,底层调用的是General SQL Parser(GSP https://sqlparser.com) 的库. 这个gsp有开源的java demo项目:https://github.com/sqlparser/gsp_demo_java 1.快速使用…

Focus-DETR利用双重注意力机制重建编码器,打造最强目标检测模型

前期的文章我们介绍了DETR模型,我们知道DETR模型首先使用CNN卷积神经网络搜集图片的核心特征点,然后把这些特征点整合起来,通过embedding方法,把特征图片转换到特征向量空间。然后根据标准Transformer模型的编码器与解码器进行注意力机制的计算,最后把计算后的数据进行图片…

vue3+ts+vite使用el-breadcrumb实现面包屑组件,实现面包屑过渡动画

简介 使用 element-plus 的 el-breadcrumb 组件&#xff0c;实现根据页面路由动态生成面包屑导航&#xff0c;并实现面包屑导航的切换过渡动画 一、先看效果加粗样式 1.1 静态效果 1.2 动态效果 二、全量代码 <script lang"ts" setup> import { ref, watch…

5个高清视频素材网站

推荐5个高清视频素材网站&#xff0c;免费、付费、商用的都有&#xff0c;可根据自己需求去选择&#xff0c;赶紧收藏吧&#xff01; 菜鸟图库 https://www.sucai999.com/video.html?vNTYxMjky ​ 菜鸟图库网素材非常丰富&#xff0c;网站主要还是以设计类素材为主&#xff…

RCE远程命令执行

逻辑运算符:: &&&#xff1a;代表首先执行命令a&#xff0c;若成功再执行命令b&#xff0c;又被称为短路运算符。 &&#xff1a;代表首先执行命令a再执行命令b&#xff0c;不管a是否成功&#xff0c;都会执行命令b。在执行效率上来说“&&”更加高效。 ||&a…

合宙Air724UG LuatOS-Air LVGL API--简介

为何是 LVGL LVGL 是一个开源的图形库&#xff0c;它提供了创建嵌入式 GUI 所需的一切&#xff0c;具有易于使用的图形元素、漂亮的视觉效果和低内存占用的特点。 LVGL特点&#xff1a; 强大的 控件 &#xff1a;按钮、图表、列表、滑动条、图像等 高级图形引擎&#xff1a;动…

AIGC之文本内容生成概述(下)—— BERT

关于AIGC系列的文章内容&#xff0c;我们在上一期介绍了基础模型Transformer&#xff0c;本期将会继续介绍基于Transformer模型改进的BERT&#xff08;双向编码器表示Transformer&#xff09;模型。如果想要查之前的介绍内容&#xff0c;可以关注本号&#xff0c;翻看之前的文章…

九耶丨阁瑞钛伦特-在项目中找到的经典BUG是什么?

在项目中找到的经典BUG有很多种&#xff0c;以下是其中一些常见的例子&#xff1a; 空指针异常&#xff08;NullPointerException&#xff09;&#xff1a;当程序试图访问一个空对象或未初始化的变量时&#xff0c;会抛出空指针异常。这通常是由于缺少对变量的正确初始化或检查…