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

本章重点

  • 链表的分类

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

  • 顺序表和链表的区别

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

一、链表的分类

实际中链表的结构非常多样,以下情况组合起来就有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. 链表:链表是一种通过节点和指针连接元素的数据结构,它的元素在内存中不一定是连续存储的。在链表中,每个节点都包含了数据以及指向下一个节点的指针。这种非连续的存储方式可能导致空间局部性较差,因为在访问一个节点时,并不保证它的相邻节点会被加载到高速缓存中。这会导致更频繁的主存访问,从而降低了效率。

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

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

本章结束啦!!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PHP8的字符串操作3-PHP8知识详解

今天继续分享字符串的操作&#xff0c;前面说到了字符串的去除空格和特殊字符&#xff0c;获取字符串的长度&#xff0c;截取字符串、检索字符串。 今天继续分享字符串的其他操作。如&#xff1a;替换字符串、分割和合成字符串。 5、替换字符串 替换字符串就是对指定字符串中…

改进YOLO系列:3.添加SOCA注意力机制

添加SOCA注意力机制 1. SOCA注意力机制论文2. SOCA注意力机制原理3. SOCA注意力机制的配置3.1common.py配置3.2yolo.py配置3.3yaml文件配置1. SOCA注意力机制论文 暂未找到 2. SOCA注意力机制原理 3. SOCA注意力机制的配置 3.1common.py配置 ./models/common.p…

C#程序随系统启动例子 - 开源研究系列文章

今天讲讲C#中应用程序随系统启动的例子。 我们知道&#xff0c;应用程序随系统启动&#xff0c;都是直接在操作系统注册表中写入程序的启动参数&#xff0c;这样操作系统在启动的时候就根据启动参数来启动应用程序&#xff0c;而我们要做的就是将程序启动参数写入注册表即可。此…

react-native-webview使用postMessage后H5不能监听问题(iOS和安卓的兼容问题)

/* 监听rn消息 */ const eventListener nativeEvent > {//解析数据actionType、extraconst {actionType, extra} nativeEvent.data && JSON.parse(nativeEvent.data) || {} } //安卓用document&#xff0c;ios用window window.addEventListener(message, eventLis…

SASS 学习笔记 II

SASS 学习笔记 II 上篇笔记&#xff0c;SASS 学习笔记 中包含&#xff1a; 配置 变量 嵌套 这里加一个扩展&#xff0c;嵌套中有一个 & 的用法&#xff0c;使用 & 可以指代当前 block 中的 selector&#xff0c;后面可以追加其他的选择器。如当前的 scope 是 form&a…

02-C++数据类型-高级

数据类型-高级 4、复合类型 4.4、结构简介 struct inflatable {char name[20];float vol;double price; };inflatable vincent; //C struct inflatable goose; //C例子 // structur.cpp -- a simple structure #include <iostream> struct inflatable // structu…

17万字数字化医院信息化建设大数据平台建设方案WORD

导读&#xff1a;原文《17万字数字化医院信息化建设大数据平台建设方案WORD》&#xff08;获取来源见文尾&#xff09;&#xff0c;本文精选其中精华及架构部分&#xff0c;逻辑清晰、内容完整&#xff0c;为快速形成售前方案提供参考。 目录 第1章 医院信息化概述 1.1 国内…

Spring的生命周期及Spring Bean单例和多例---超详细教学

一&#xff0c;何为spring生命周期 一个Bean对象从被Spring容器创建到被销毁的整个过程。Spring框架对Bean对象的生命周期进行了管理&#xff0c;提供了灵活性和控制权&#xff0c;让开发人员能够在不同的阶段进行自定义操作 1.1生命周期图 1.2.为什么要学习对象的生命周期…

PyTorch训练深度卷积生成对抗网络DCGAN

文章目录 DCGAN介绍代码结果参考 DCGAN介绍 将CNN和GAN结合起来&#xff0c;把监督学习和无监督学习结合起来。具体解释可以参见 深度卷积对抗生成网络(DCGAN) DCGAN的生成器结构&#xff1a; 图片来源&#xff1a;https://arxiv.org/abs/1511.06434 代码 model.py impor…

Docker容器:docker的资源控制及docker数据管理

文章目录 一.docker的资源控制1.CPU 资源控制1.1 资源控制工具1.2 cgroups有四大功能1.3 设置CPU使用率上限1.4 进行CPU压力测试1.5 设置50%的比例分配CPU使用时间上限1.6 设置CPU资源占用比&#xff08;设置多个容器时才有效&#xff09;1.6.1 两个容器测试cpu1.6.2 设置容器绑…

马上七夕到了,用各种编程语言实现10种浪漫表白方式

目录 1. 直接表白&#xff1a;2. 七夕节表白&#xff1a;3. 猜心游戏&#xff1a;4. 浪漫诗句&#xff1a;5. 爱的方程式&#xff1a;6. 爱心Python&#xff1a;7. 心形图案JavaScript 代码&#xff1a;8. 心形并显示表白信息HTML 页面&#xff1a;9. Java七夕快乐&#xff1a;…

【学习笔记之vue】These dependencies were not found:

These dependencies were not found:方案一 全部安装一遍 我们先浅试一个axios >> npm install axios 安装完报错就没有axios了&#xff0c;验证咱们的想法没有问题&#xff0c;实行&#xff01; ok

city walk结合VR全景,打造新时代下的智慧城市

近期爆火的city walk是什么梗&#xff1f;它其实是近年来备受追捧的城市漫步方式&#xff0c;一种全新的城市探索方式&#xff0c;与传统的旅游观光不同&#xff0c;城市漫步更注重与城市的亲密接触&#xff0c;一步步地感受城市的脉动。其实也是一种自由、休闲的方式&#xff…

【Windows系统编程】03.远线程注入ShellCode

shellcode&#xff1a;本质上也是一段普通的代码&#xff0c;只不过特殊的编程手法&#xff0c;可以在任意环境下&#xff0c;不依赖于原有的依赖库执行。 远程线程 #include <iostream> #include <windows.h> #include <TlHelp32.h>int main(){HANDLE hPr…

stable diffusion基础

整合包下载&#xff1a;秋叶大佬 【AI绘画8月最新】Stable Diffusion整合包v4.2发布&#xff01; 参照&#xff1a;基础04】目前全网最贴心的Lora基础知识教程&#xff01; VAE 作用&#xff1a;滤镜微调 VAE下载地址&#xff1a;C站&#xff08;https://civitai.com/models…

小程序的数据绑定和事件绑定

小程序的数据绑定 1.需要渲染的数据放在index.js中的data里 Page({data: {info:HELLO WORLD,imgSrc:/images/1.jpg,randomNum:Math.random()*10,randomNum1:Math.random().toFixed(2)}, }) 2.在WXML中通过{{}}获取数据 <view>{{info}}</view><image src"{{…

爬虫逆向实战(二)--某某观察城市排行榜

一、数据接口分析 主页地址&#xff1a;某某观察 1、抓包 通过抓包可以发现数据接口是multi 2、判断是否有加密参数 请求参数是否加密&#xff1f; 无请求头是否加密&#xff1f; 无cookie是否加密&#xff1f; 无响应数据是否加密&#xff1f; 通过查看“响应”板块可以…

windows10 安装WSL2, Ubuntu,docker

AI- 通过docker开发调试部署ChatLLM 阅读时长&#xff1a;10分钟 本文内容&#xff1a; window上安装ubuntu虚拟机&#xff0c;并在虚拟机中安装docker&#xff0c;通过docker部署数字人模型&#xff0c;通过vscode链接到虚拟机进行开发调试.调试完成后&#xff0c;直接部署在云…

如何搭建游戏服务器?有哪些操作步骤

​  选择游戏服务器提供商 为确保游戏服务器的稳定运行和及时响应问题&#xff0c;选择一个正规、靠谱的游戏服务器提供商非常重要。 选择服务器操作系统 根据不同游戏的需求&#xff0c;选择适合的操作系统&#xff0c;通常可选择Linux或WindowsServer操作系统。 上传、安装…

【C#】条码管理操作手册

前言&#xff1a;本文档为条码管理系统操作指南&#xff0c;介绍功能使用、参数配置、资源链接&#xff0c;以及异常的解决等。思维导图如下&#xff1a; 一、思维导图 二、功能操作–条码打印&#xff08;客户端&#xff09; 2.1 参数设置 功能介绍&#xff1a;二维码图片样…