【数据结构】顺序表+链表

目录

1.顺序表

1.1初始化顺序表

1.2销毁顺序表

1.3检查容量并扩容

1.4把某个元素插入到下标为pos的位置

1.5头插和尾插

1.6删除下标为pos的元素

1.7头删和尾删

2.顺序表的问题及思考

3.链表

3.1链表的访问

3.2链表的增删查改


1.顺序表

顺序表的本质其实就是一个数组,实际上如果要对数组中某块空间进行删除是非常不方便的,因为如果要删就只能全删掉,或者用后面的覆盖前面的,从效果上来看是删掉了某个元素,那既然顺序表这么不方便,为什么还要使用顺序表?是因为顺序表有一个压倒性的优势就是能够通过下标来访问到我们需要的元素。

顺序表包括静态顺序表和动态顺序表,静态顺序表就是一个固定长度的数组,动态顺序表就是用malloc开辟的一块数组空间存储。实际上我们以前用C语言实现的动态版本的通讯录就充分使用了顺序表,以实现他的增删查改等功能。

下面来演示操作一个顺序表的常用操作

这是一个头文件sl.h,用来声明顺序表的类型与各种函数

接下来完成这些函数功能

1.1初始化顺序表

使用的是动态内存开辟的方式,刚上来a数组能放四个元素

1.2销毁顺序表

1.3检查容量并扩容

a的内存是通过malloc申请的,后面还可能会通过realloc来调整这块空间的大小。因此使用完成之后应该free。

在插入的时候,应该检查顺序表是否已满,如果满了,就扩容

一次性扩容成原来容量的两倍

1.4把某个元素插入到下标为pos的位置

插入之前应该先检查顺序表是否已经满了,为了防止插入的下标合法应该assert一下,当然也可以使用if语句判断,这里我用的是assert

1.5头插和尾插

把某个元素插入到顺序表最开头

可以单独写,就像上图中我注释掉的那段代码一样,先让所有元素往后挪动一位,然后把要插入的元素赋给下标为零的元素覆盖掉原来的值。

当然也可以直接调用刚才我们写的在中间插入元素的函数,头插实际上就是在下标为零的位置插入。

同理尾插就是在下标size的位置插入。

1.6删除下标为pos的元素

先找到这个元素,然后他后面的所有元素往前挪动一位,覆盖掉他,最后size--

1.7头删和尾删

头删和尾删的实现就可以直接调用该函数

头删就是删除下标为0的元素,尾删就是删除下标为size-1的元素

2.顺序表的问题及思考

问题:

1. 中间/头部的插入删除,时间复杂度为O(N)

2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。

3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

思考:如何解决以上问题呢?下面给出了链表的结构来看看。

3.链表

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。

前面管理顺序表的时候,我们定义的结构体需要三个参数,第一个就是一个数组,用来存放数据,第二个是sz,用来记录这个数组中已经有了几个元素了,第三个是capacity,用来记录当前数组的最大容量。定义链表的结构体类型又需要哪些成员变量呢?首先当前内存中应该存着某个数值,而非数组,又因为链表在物理存储上并非连续,要从当前内存找到下一块内存,就需要一个指针,这个指针一般命名为next,而且链表我们,因此链表的结构体类型一般是这样

存着一个数值和一个同种结构体类型的指针,(结构体可以嵌套结构体指针,但是不能嵌套结构体类型,因为这样就会无限套娃而无法计算结构体类型的大小了)

3.1链表的访问

可以这样访问链表

这样的每一个单独的空间就是一个节点,上面的一个节点就包括了一个数值和一个指针,用来指向下一个节点。

在用函数操作链表之前,一定要明确是要修改结构体,还是要修改结构体指针,如果是修改结构体,在函数内应该使用结构体指针,如果是要修改结构体指针(一般是某个节点的地址),就要使用结构体指针的地址,也就是一个二级指针。由于我们经常需要改变某个节点的地址,因此我们会经常传一个结构体指针的地址。比如想要在链表头部插入一个节点,而我们用一个名为phead的结构体指针表示链表头部的地址,指向第一个节点,在我们头插一个节点之后我们显然要改变这个phead,也就是要改变结构体指针,那我们在传参的时候就应该传一个结构体指针的地址。

3.2链表的增删查改

如果想要在链表最前面插入一个数据,应该如何做?

只需要让新malloc的那块空间的节点指向原来最前面的那块空间即可。

SLTNode是我们刚才重命名的那个结构体类型,我们要在链表的头部插入一个新的元素,这个新的元素也是一个结构体类型,包含一个数值和一个指针,这个指针指向原来的phead。于是我们就是malloc了一块空间,大小能放一个SLTNode类型的结构体变量(绝对不能直接写成SLTNode*newnode,因为这是一个局部变量,调用完函数之后就销毁了要让新开辟的这块空间一直存在,应该使用malloc),newnode里面就是新开辟的空间的地址,但是这样写虽然开辟的空间一直存在,但是仍然有问题,比如我现在要测试一下

我们的预期应该是打印出来1 2 3 4,但是实际上什么也打印不出来,因为这是一个传值调用,你可能有疑问,这不是传的地址吗?怎么还是传值调用?这个plist是一个结构体指针,指向了某一个结构体,他是我们创建的一个局部变量,在这个TestSlist1函数中由于刚上来这个plist的值是NULL,我们可以认为他是链表的最后一个节点,现在我们要在这个节点的前面插入四个节点,内容分别是1,2,3,4以及下一个节点的地址,虽然plist是一个指针变量,里面存的是一个地址,但是仍然是一个变量,现在直接把plist传给了SLPushFront,当然是传值调用,SLPushFront函数会创建一个形参叫做phead,并把plist里面相同的值拷贝进去,然后SLPushFront函数里面又创建了一个newnode变量,把他和phead一顿操作,那也只是在SLPushFront的栈上进行的操作,根本不会影响plist的值,因此最终plist还是NULL,当然什么也打印不出来。真要传址调用,应该传&plist,我再说的通俗一点,如果我们想要通过函数改变int类型的,我们应该传int*类型的,那么我们现在想要用函数来改变SLTNode*类型的变量plist,我们应该传给函数的参数类型是SLTNode**类型。

回到我们的SLPushFront函数,要想通过他添加节点,并把原来phead改变,形参应该是phead的地址,也就是SLTNode**类型

对SLPushFront进行修改

然后测试的时候传plist的地址

即可实现预期

如果对传址调用的理解还没有那么深刻,那我再举一个尾插函数的例子

首选因为不管是头插还是尾插,都需要动态申请一块内存空间,为了避免代码的冗余,我们利用一个BuyLTNode函数来实现该功能

尾插的函数假如我这么写

首先我们创建了一个局部变量tail并初始化为phead也就是第一个节点的地址,那现在tail指向了第一个节点,要判断tail是否为尾部节点,就是看tail->next是否为NULL,于是利用while循环来找到这个尾部的节点,出循环之后,tail就指向了最后一个节点,我们使用BuyLTNode函数申请一个节点并把这个节点的地址赋给tail,由于使用BuyLTNode申请的节点中next已经被初始化成了NULL,那么当我们把这个地址赋给tail之后,tail就指向了一个节点,这个节点的next是NULL,data被初始化成了x,也就是我们希望插入的数值。此时tail指向了链表最后一个节点,完成了尾插操作。

但事实上这个代码并不能完成尾插的工作,因为我们这里不管是tail也好,还是newnode也好,都是局部变量,出了这个函数之后,这些局部变量就被销毁了,而我们使用BuyLTNode申请的那块空间却还在,但是这块空间却无法找到了,造成了内存泄漏。而且还有一个问题就是,当tail指向最后一个节点的时候,把这个节点的next也就是NULL赋给tail,这时候tail什么也不指向,我们也无法将tail->next修改掉,因此这种写法也无法把链表的节点连接起来。

如果把while循环的判断条件改成tail->next呢?

那就要看tail->next什么时候是NULL,我们发现此时tail指向的是最后一个节点,而非NULL,这样如果我们把tail->next改成尾插的节点的地址,就可以实现链表节点的连接。这样是不是就可以了了?

这样其实已经能够完成大部分情况下的尾插了,我们动态申请了一个节点,然后把这个节点的地址放在了newnode里面,又把newnode赋给了tail->next,也就是说本来tail指向的是NULL,现在tail指向的就是动态申请的这块空间,调用结束之后虽然newnode,tail等变量被销毁,但是链表的所有节点都在堆区上,因此所有节点的内容(data和next)都不会被销毁,也就是说内容已经改完了,也已经尾插了一个节点,虽然此时的用来指向链表头部的形参plist已经被销毁,但是好在这个plist传值调用只是链表首地址的一份临时拷贝,他与我们传的实参里面存放的内容是一样的,都是链表头部的地址,既然已经在堆区上完成了尾插的工作,我们当然可以通过传的实参来实现链表的打印。

但是如果刚上来链表是空的,也就是说plist是NULL,如果还是按照上面代码的逻辑,把空指针赋给了tail,然后是while的判断条件,这将会对NULL进行解引用,显然是不对的,那么我们能不能使用if语句把链表为空和非空这两种情况分开来看?也就是说现在改成了这样子

通过测试发现这种方式也不行,这是因为plist刚上来是NULL,我们要尾插一个节点,并让plist指向这个节点,也就是说我们想改变plist,但是我们在调用SListPushBack函数的时候却使用了传值调用,直接把plist的值传了过去,这样是无法改变plist的,要想在函数内改变plist,我们只能在传参的时候使用传址调用,把plist的地址传过去,使用一个SListNode**类型的二级指针来接收这个地址。正确写法如下

我们在传参的时候需要传一个节点的地址也就是&plist,并存放在pplist里面,这个指针就指向了plist,对他进行解引用就找到了plist,如果plist是NULL,那么执行的将会是把newnode赋给*pplist,这个*pplist就不是实参的一份临时拷贝了,而是确确实实是实参所在的单元,通过这个指针就可以改变这个单元的内容,也就当然可以修改plist的内容了。

注:如果要改结构体,需要用结构体指针,如果要改结构体指针,需要用结构体指针的地址,在尾插的函数中,只有链表为空的时候需要修改plist也就是结构体指针,当链表不为空的时候,修改的其实是节点里面的next也就是结构体。

要改变什么,就要用他的地址,并在函数里面对这个地址进行解引用

删除尾部节点

先考虑链表包含一个以上节点的情况,我们要做的是释放掉最后一个节点,并把倒数第二个节点的next置空,这个过程中我们改变的是结构体,应该使用结构体指针。

想要删除尾部节点首先应该找到尾部的节点,当tail指向尾部节点的时候free(tail),就可以把尾部节点这个空间释放掉,在使用free函数之后,通常要把tail置为空指针,实际上tail作为一个局部变量,置空与否都可以,因为出了这个函数tail变量就被销毁了,并不会产生访问他的情况,实际上我们应该把原来倒数第二个节点的next置为NULL,要改变节点,也就是结构体的一部分,我们应该使用结构体指针,于是我们使用了一个指针prev来找到倒数第二个节点并将这个节点的next置空。如图

尾删还有一种写法就是我们直接去找倒数第二个节点,如图

当链表只有一个节点,我们要做的是释放掉这个节点,并让plist置为NULL,因此我们是要改变结构体指针,应该使用结构体指针的地址,这也是我们把函数的形参设计成二级指针的原因。因此完整的尾删功能代码如下

同理头删也需要分三种情况讨论

实际上上面的代码完全可以把链表只有一个节点和链表有多个节点的情况合并起来,如图

只有一个节点也就是*pplist->next是NULL,先把*pplist也就是plist也就是指向唯一节点的这个指针拷贝给tmp,然后把tmp->next也就是NULL赋给*pplist也就是plist,这时候原本指向唯一节点的结构体指针就指向了NULL,与我们期望的逻辑相同。

单链表查找

找查的时候并不需要修改链表中的节点或者指针,因此也并不需要传二级指针,唯一需要注意的点就是while循环的判断条件,如果写成cur->next,当它为NULL的时候实际上cur指向了最后一个节点,而我们这时候没有进入while循环,因此就没有判断最后一个节点的data是不是我们要找的内容。

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

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

相关文章

Tensorflow2.0笔记 - 计算梯度

本笔记主要记录tf.GradientTape和tf.gradient的用法 import tensorflow as tf import numpy as nptf.__version__#要计算梯度的所有参数计算过程必须放到gradient tape中 #with tf.GradientTape as tape: w tf.constant(1.) x tf.constant(2.)with tf.GradientTape() as tap…

惯性导航 | 测量方程中的噪声模型与离散时间噪声模型

惯性导航 | 测量方程中的噪声模型与离散时间噪声模型 IMU测量方程中的噪声模型IMU的离散时间噪声模型 IMU测量方程中的噪声模型 在大多数系统中,IMU的噪声由两部分组成:测量噪声(Measurement Nosie)与零偏(Bias&#…

1.1_3 性能指标——时延、时延带宽积、往返时间RTT、利用率

文章目录 1.1_3 性能指标——时延、时延带宽积、往返时间RTT、利用率(一)时延(二)时延带宽积(三)往返时延RTT(四)利用率 1.1_3 性能指标——时延、时延带宽积、往返时间RTT、利用率 …

wireshark抓取localhost(127.0.0.1)数据包

打开wireshark中,在"capture"菜单中,选择"interfaces"子菜单,在列出的接口中选中"Adapter for loopback traffic capture"即可。 必须安装了Npcap才有此选项,否则需要重新安装wireshark。 抓包截图…

桥梁安全监测方案:多维度的技术与设备应用

一、背景与意义 随着交通基础设施的快速发展,桥梁作为连接两岸的关键纽带,其安全性能直接关系到人们的生命财产安全和交通畅通。为确保桥梁的安全与稳定,及时发现潜在的安全隐患,采取科学、有效的安全监测方案显得尤为重要。本文将…

材料物理 (HIT) 笔记-2

原内容请参考哈尔滨工业大学何飞教授:https://www.bilibili.com/video/BV18b4y1Y7wd/?p12&spm_id_frompageDriver&vd_source61654d4a6e8d7941436149dd99026962 或《材料物理性能及其在材料研究中的应用》(哈尔滨工业大学出版社) 三…

进程间通信之信号灯 || 网络协议UDP/TCP || 三次握手四次挥手

在线程通信中由于数据段等内存空间的共用性,导致同时访问时资源竞争的问题,在线程中我们使用信号量的申请和释放,在防止资源竞争的产生。在进程间的通信中,有信号灯的概念。搭配共享内存实现进程同步。 有名信号量: 1.创建 …

HTML标签语义化,含面试题+答案

form表单细节 一、表单 1.表单 标签用于为用户输入创建 HTML 表单 2.表单能够包含 input 元素,比如文本字段、复选框、单选框、提交按钮等等。 3.表单还可以包含 menus、textarea、fieldset、legend 和 label 元素。 4.表单用于向服务器传输数据。 二、表单form 的…

相机类型的分辨率长宽、靶面尺寸大小、像元大小汇总

镜头的靶面尺寸大于等于相机靶面尺寸。 相机的芯片长这样,绿色反光部分(我的手忽略): 基本所有像素的相机的靶面大小都可以在这个表格里面找到。 镜头的靶面尺寸在镜头外表上可以找到,选型很重要!

代码还原之 函数

指令堆里逆向出来的代码有歧义&#xff0c;有三处返回&#xff0c;有嵌套IF语句&#xff0c;故推断出是个函数&#xff1b; #if 0/*27ec: 48 8d 3d 58 39 00 00 lea 0x3958(%rip),%rdi # 614b <_IO_stdin_usedBase0x14b> // rdi"COLUMNS"27f3: e8 e…

【鸿蒙 HarmonyOS 4.0】多设备响应式布局

一、背景 在渲染页面时&#xff0c;需要根据不同屏幕大小渲染出不同的效果&#xff0c;动态的判断设备屏幕大小&#xff0c;便需要采用多设备响应式布局。这种设计方法能够动态适配各种屏幕大小&#xff0c;确保网站在不同设备上都能呈现出最佳的效果。 二、媒体查询&#xf…

第九篇:– 过程发现(Process Discovery)是如何赋能数字化市场营销全过程?- 我为什么要翻译介绍美国人工智能科技巨头IAB公司

IAB平台&#xff0c;使命和功能 IAB成立于1996年&#xff0c;总部位于纽约市。 作为美国的人工智能科技巨头社会媒体和营销专业平台公司&#xff0c;互动广告局&#xff08;IAB- the Interactive Advertising Bureau&#xff09;自1996年成立以来&#xff0c;先后为700多家媒体…

【ELK日志分析系统】ELK+Filebeat分布式日志管理平台部署

ELKFilebeat部署一、ELK简介1、ELK组件1.1 其他组件 2、为什么要使用 ELK3、完整日志系统基本特征 二、ELK的工作原理三、ELK Elasticsearch 集群部署1、环境准备2、部署 Elasticsearch 软件(node节点)2.1 安装elasticsearch—rpm包2.2 修改elasticsearch主配置文件2.3 es性能调…

Nerf原理理解

神经辐射场是一个简单的全连接网络&#xff08;权重约为 5MB&#xff09;&#xff0c;经过训练可使用渲染损失再现单个场景的输入视图。该网络直接从空间位置和观看方向&#xff08;5D 输入&#xff09;映射到颜色和不透明度&#xff08;4D 输出&#xff09;&#xff0c;充当“…

AntV L7初体验

本案例使用L7库和Mapbox GL JS创建的简单地图可视化示例&#xff0c;加载点数据。 文章目录 1. 引入 CDN 链接2. 导出模块3. 创建地图3.1. 注册 token3.2. 创建地图实例 4. 创建场景5.创建点图层6. 演示效果7. 代码实现 1. 引入 CDN 链接 <!-- 1.引入CDN链接 --> <!--…

数据库期末速成100分训练,附练手数据库原件及教程

本文提供下面数据库代码的数据库原件&#xff0c;下载后可使用 教程如下&#xff1a; 1.打开sql sever 2.找到数据库 3.右键数据库点击“附加”&#xff0c;然后点击“添加” 4.导入数据库原件&#xff0c;点击确定 ps&#xff1a;如果没有sqlsever 或者页面编辑器&#x…

manjaro 安装 wps 教程

内核: Linux 6.6.16.2 wps-office版本&#xff1a; 11.10.11719-1 本文仅作为参考使用, 如果以上版本差别较大不建议参考 安装wps主体 yay -S wps-office 安装wps字体 &#xff08;如果下载未成功看下面的方法&#xff09; yay -S ttf-waps-fonts 安装wps中文语言 yay …

uniapp 手写 简易 时间轴 组件

一、案例如图 该案例设计条件&#xff1a; 左侧时间 和竖线、点、内容都是居中对其的&#xff0c;上下时间点中间要有一段距离 二、编写逻辑 1. 布局结构&#xff1a;一共三个元素&#xff0c;左侧是时间和黑点&#xff0c;中间是线条&#xff0c;右侧是内容 2. 样式难点&#…

《UE5_C++多人TPS完整教程》学习笔记27 ——《P28 项目资产(Assets for The Project)》

本文为B站系列教学视频 《UE5_C多人TPS完整教程》 —— 《P28 项目资产&#xff08;Assets for The Project&#xff09;》 的学习笔记&#xff0c;该系列教学视频为 Udemy 课程 《Unreal Engine 5 C Multiplayer Shooter》 的中文字幕翻译版&#xff0c;UP主&#xff08;也是译…

c# 调用ip2region组件 根据ip地址进行定位归属地运营商

需求描述&#xff1a;当项目中需要将IP转换成对应的归属地以及运营商&#xff0c;那么通过ip2region组件即可完美实现。 p2region本身支持net4.5以上&#xff0c;还有个ip2region.net组件&#xff0c;它要求net6及以上。所以&#xff0c;根据自己项目的需求即可选择其中一种方…