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

news2024/11/27 12:42:51

目录

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.coloradmin.cn/o/1492665.html

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

相关文章

Java | vscode如何使用命令行运行Java程序

1.在vscode中新建一个终端 2.在终端中输入命令 输入格式&#xff1a; javac <源文件>此命令执行后&#xff0c;在文件夹中会生成一个与原java程序同名的.class文件。然后输入如下命令&#xff1a; java <源文件名称>这样java程序就运行成功了。&#x1f607;

【LeetCode每日一题】【BFS模版与例题】【二维数组】1293. 网格中的最短路径

BFS基本模版与案例可以参考&#xff1a; 【LeetCode每日一题】【BFS模版与例题】863.二叉树中所有距离为 K 的结点 【LeetCode每日一题】【BFS模版与例题】【二维数组】130被围绕的区域 && 994 腐烂的橘子 思路&#xff1a; 特殊情况&#xff1a; 最短的路径是向下再向…

云计算科学与工程实践指南--章节引言收集

云计算科学与工程实践指南–章节引言收集 //本文收集 【云计算科学与工程实践指南】 书中每一章节的引言。 我已厌倦了在一本书中阅读云的定义。难道你不失望吗&#xff1f;你正在阅读一个很好的故事&#xff0c;突然间作者必须停下来介绍云。谁在乎云是什么&#xff1f; 通…

#QT(TCP网络编程-服务端)

1.IDE&#xff1a;QTCreator 2.实验&#xff1a;编写一个tcp服务端 QTcpsever QTcpsocket 3.记录&#xff1a; (1)先搭建界面 &#xff08;2&#xff09;服务端代码 a. pro QT core gui networkgreaterThan(QT_MAJOR_VERSION, 4): QT widgetsCONFIG c1…

html实体字符,已拿offer入职

面试知识点 主要内容包括html&#xff0c;css&#xff0c;前端基础&#xff0c;前端核心&#xff0c;前端进阶&#xff0c;移动端开发&#xff0c;计算机基础&#xff0c;算法与数据结构&#xff0c;设计模式&#xff0c;项目等等。 html 1.浏览器页面有哪三层构成&#xff0c…

video视频播放

1.列表页面 <template><div><ul><li class"item" v-for"(item,index) in list" :key"index" click"turnPlay(item.videoUrl)"><img :src"item.img" alt""><div class"btn…

Go 简单设计和实现可扩展、高性能的泛型本地缓存

相信大家对于缓存这个词都不陌生&#xff0c;但凡追求高性能的业务场景&#xff0c;一般都会使用缓存&#xff0c;它可以提高数据的检索速度&#xff0c;减少数据库的压力。缓存大体分为两类&#xff1a;本地缓存和分布式缓存&#xff08;如 Redis&#xff09;。本地缓存适用于…

论文目录3:大模型时代(2023+)

1 instruction tuning & in context learning 论文名称来源主要内容Finetuned Language Models Are Zero-Shot Learners2021 机器学习笔记&#xff1a;李宏毅ChatGPT Finetune VS Prompt_UQI-LIUWJ的博客-CSDN博客 早期做instruction tuning的work MetaICL: Learning to …

CSS文本样式值,web前端开发资料

正文 什么是行内元素&#xff1f; display属性为inline的元素为行内元素&#xff0c;英文&#xff1a;inline element&#xff0c;其中文叫法有多种&#xff0c;如&#xff1a;内联元素、内嵌元素、行内元素、直进式元素等。 特点&#xff1a; 和其他元素都在一行上&#x…

补点基础——几何尺寸和公差

几何尺寸与公差&#xff08;Geometric dimensioning and tolerancing&#xff09;代号&#xff1a;GD&T&#xff0c;主要是以下两个组织制定&#xff1a; 1.美国机械工程师学会&#xff08;ASME&#xff09; ASME Y14.5 是 GD & T 的完整定义&#xff0c;其中包含所有…

mac报错:zsh:command not found: brew

1、基本概述&#xff1f; 在使用brew安装程序的时候MAC提示&#xff1a; zsh:command not found: brew 本质就是brew没有安装&#xff0c;这个命令与linux系统中的yum命令类似。 使用的环境说明&#xff1a; 虚拟机版本&#xff1a;VMware Workstation 17 Pro mac os Ventu…

基于ERNIR3.0文本分类的开发实践

参考&#xff1a;基于ERNIR3.0文本分类&#xff1a;(KUAKE-QIC)意图识别多分类(单标签) - 飞桨AI Studio星河社区 (baidu.com) https://zhuanlan.zhihu.com/p/574666812?utm_id0 遇到的问题&#xff1a;如下 采用paddleNLP下文本分类实例进行分类训练后发现 生成的模型分类不…

组基轨迹建模 GBTM的介绍与实现(Stata 或 R)

基本介绍 组基轨迹建模&#xff08;Group-Based Trajectory Modeling&#xff0c;GBTM&#xff09;&#xff08;旧名称&#xff1a;Semiparametric mixture model&#xff09; 历史&#xff1a;由DANIELS.NAGIN提出&#xff0c;发表文献《Analyzing Developmental Trajectori…

【软件架构的常用分类及建模方法】

曾梦想执剑走天涯&#xff0c;我是程序猿【AK】 目录 简述概要知识图谱 简述概要 了解软件架构的常用分类及建模方法 知识图谱 1.1.2 软件架构的常用分类及建模方法 软件架构的常用分类 多年来&#xff0c;“架构”概念经过不断演化&#xff0c;目前已形成了满足不同用途的…

Meta AI最近推出的新模型,Searchformer

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

echarts柱状图可鼠标左击出现自定义弹框,右击隐藏弹框并阻止默认右击事件

每项x轴数据对应有两条柱图和一条阴影效果是学习其它博客得到的效果&#xff0c;这个是学习的原文链接&#xff1a;echarts两个合并柱体&#xff08;普通柱状图象形柱图&#xff09;共享一个柱体阴影 因为这次情况比较特殊&#xff0c;不仅需要自定义弹框内容&#xff0c;而且…

Python——与Matlab对应的Python版本

参考资料&#xff1a; Python——与Matlab对应的Python版本

Android高级工程师面试实战,三幅图给你弄懂EventBus核心原理

阿里技术一面-35min 自我介绍 Android 有没有遇到OOM问题(有遇到内存泄漏问题)Handler机制ThreadLocalActivity启动到加载View过程View绘制过程LinearLayout (wrap_content) & TextView (match_parent) 最终结果???OKHttp(1. 为什么选择它&#xff1f; 2. 性能了解不…

(C++练习)选择题+编程题

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 选择题 以下程序输出结果是什么&#xff08;&#xff09; class A{public:virtual void func(int val 1){ std::cout<<"A->"<< val <<std::endl;}virtual void test(){ func();}};class B…

CSS标准文档流与脱离文档流,爆推

面试题&#xff1a; Html 1&#xff0c;html语义化 2&#xff0c;meta viewport相关 3&#xff0c;canvas 相关 CSS 1&#xff0c;盒模型 1.1&#xff0c;ie盒模型算上border、padding及自身&#xff08;不算margin&#xff09;&#xff0c;标准的只算上自身窗体的大小 c…