我们在数据结构(二),对链表和顺序表已经讲解过了。但很多同学表示有点晦涩难懂那我就出一篇深层次讲解,一步一步来带领大家学习。
我们从头(数据结构)开始完整的来为大家讲解,大家好好看好好学。定有收获
1.数据结构相关概念
1.1、什么是数据结构?
数据结构是由“数据”和“结构”两词组合⽽来。
什么是数据?
常⻅的数值1、2、3、4.....、教务系统⾥保存的⽤⼾信息(姓名、性别、年龄、学历等等)、⽹⻚⾥⾁眼可以看到的信息(⽂字、图⽚、视频等等),这些都是数据
什么是结构?
当我们想要使⽤⼤量使⽤同⼀类型的数据时,通过⼿动定义⼤量的独⽴的变量对于程序来说,可读性⾮常差,我们可以借助数组这样的数据结构将⼤量的数据组织在⼀起,结构也可以理解为组织数据的⽅式。
想要找到草原上名叫“咩咩”的⽺很难,但是从⽺圈⾥找到1号⽺就很简单,⽺圈这样的结构有效将
⽺群组织起来。
概念:数据结构是计算机存储、组织数据的⽅式。数据结构是指相互之间存在⼀种或多种特定关系
的数据元素的集合。数据结构反映数据的内部构成,即数据由那部分构成,以什么⽅式构成,以及数据元素之间呈现的结构。
总结:
1)能够存储数据(如顺序表、链表等结构)?
2)存储的数据能够⽅便查找?
2、为什么需要数据结构?
如图中所⽰,不借助排队的⽅式来管理客⼾,会导致客⼾就餐感受差、等餐时间⻓、餐厅营业混乱等情况。同理,程序中如果不对数据进⾏管理,可能会导致数据丢失、操作数据困难、野指针等情况。
通过数据结构,能够有效将数据组织和管理在⼀起。按照我们的⽅式任意对数据进⾏增删改查等操
作。最基础的数据结构:数组。
【思考】有了数组,为什么还要学习其他的数据结构?
假定数组有10个空间,已经使⽤了5个,向数组中插⼊数据步骤:?
求数组的⻓度,求数组的有效数据个数,向下标为数据有效个数的位置插⼊数据(注意:这⾥是
否要判断数组是否满了,满了还能继续插⼊吗).....?
假设数据量⾮常庞⼤,频繁的获取数组有效数据个数会影响程序执⾏效率。
结论:最基础的数据结构能够提供的操作已经不能完全满⾜复杂算法实现。
2.顺序表
2.1、顺序表的概念及结构?
2.1.1线性表
线性表( linear list )是n个具有相同特性的数据元素的有限序列。 线性表是⼀种在实际中⼴泛使
⽤的数据结构,常⻅的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的,
线性表在物理上存储时,通常以数组和链式结构的形式存储。
案例:蔬菜分为绿叶类、⽠类、菌菇类。线性表指的是具有部分相同特性的⼀类数据结构的集合
如何理解逻辑结构和物理结构?
2.1.2顺序表分类
顺序表和数组的区别:
顺序表的底层结构是数组,对数组的封装,实现了常⽤的增删改查等接⼝。
顺序表分类:
2.2.1 静态顺序表
概念:使⽤定⻓数组存储元素
我们来一点一点解释,在这串代码中我们使用了自定义的 typedef 我们把 int 重命名为 SLDateType
目的:是为了区分其他代码中的int , 还有一个原因我们可以随时改变类型,在这个局部变量内部我们可以改变类型,假设我们想把int 类型改为char类型,我们直接修改即可。它不会影响程序内部其他的char,与int。
宏的作用:我们使用define来给N赋值,目的也很简单方便修改数组的大小。
静态顺序表缺陷:空间给少了不够⽤,给多了造成空间浪费 ------我们给的空间都是定义好的,没有空间去修改,给多少就是多少,不方便我们实际的使用。
静态顺序表由于不实用我们在这里就着重讲一下动态顺序表。
好戏就要开始喽
2.2.2动态顺序表
我们来一步一步开始,首先我们创建好三个文件(一个头文件,两个源文件)。
这些文件有什么作用呢?
头文件:用来实现文件的声明与定义(定义顺序表要实现的结构\接口\方法等)
源文件:SeqList.c 具体实现接口和方法,实现程序的运行过程。
text.c 实现测试作用,测试顺序表是否可以正常运行。
我们先从头文件开始
先实现静态顺序表
过程很简单和上面的一样
再来实现动态顺序表
我们接下来实现一下动态顺序表的 增加 删除 修改的功能。
初始化过程:着重详细讲解作为一个列子。
我们在头文件中定义源文件中实现。
我们把空间都初始化为0;
初始化完成之后我们来进行测试(在text.c)文件中进行
我们创建了一个测试函数 slText01 用来实现。
当我们运行来试试是否初始化完成 ?
这是出了什么问题呢?
我们回头看看到底是哪出了问题
在传值的过程中我们会创建一个临时变量图中得到红色小方块,先把值放入这个临时创建的空间中然后完成调用。
那么为什么会出现问题呢?
因为我们没有对sl 进行初始化(意味着sl是个无效值),我们没办法把值给拷贝下来放入临时创建的空间中传给s。
在这里我们就没办法进行传值操作了,只能进行传地址操作。
所以我们可以这样修改一下:
我们传地址然后使用指针来接收。
由于是指针类型我们不能继续使用点(.)来调用,而是使用指针类型的 -> 。
当我们程序跑起来观察
程序没有问题,我们通过监视来看是否初始化成功
初始化之前:
初始化之后:
都变成了0,证明我们初始化成功。
我们对空间进行开辟(动态开辟)。数据的插入
头插 void SLPushFront(SL* ps, SLDataType x);
尾插 void SLPushBack(SL* ps, SLDataType x);
画图来进行分析:
我们一个一个来讲解:
尾差
尾插有三种情况还是画图来展示:
大家肯定会有一个疑问,size不是有效数据的大小吗?在这里为什么是即将插入的下标呢?
其实size是有效元素的个数,但是也可以作为下一个即将插入元素的下标。
size就是下一个即将插入元素的下标,插入元素的时候直接用size作为下标,访问进行赋值
获取有效元素个数的时候,直接使用size。
扩容的原则:
关于动态内存管理我之前发布过专题博客很详细,感兴趣的可以看看,链接放下面了。
CSDNhttps://mp.csdn.net/mp_blog/creation/editor/136505981
关于为什么扩容大多数都是扩容二倍这个问题,现在的我能力有限还无法为大家解释清楚,但是请大家相信我,有朝一日我肯定会弄明白。
那我们接着继续:
空间足够时:
直接插入:关于为什么是size,上面有解释这里就不过多说了
size++是怎么回事呢? 是因为当我们插入一个数据的时候,size(这里指的是有效数据)就会增加1,所以我们需要++。
空间不够时:
我们可以看到size现在指向的是6的位置,我们在它的后面进行扩容。
我们来观察一下这个代码有没有问题。
仔细观察是有问题的,因为我们在初始化capacity是给的是0,在这里二倍*0还是0.
无法达到扩容的效果。
我修改了一下,大家看一下是否还是后问题:
答案是还是有问题,因为在这里我们使用三目操作符给的值是4个比特位,但是我在前面定义的是int类型。我们可以这样修改一下。
我们申请之后还需要判断是否申请成功,我们先创建一个临时变量来存放,如果申请成功了,我们就赋给newCapacity,如果没有申请成功就返回失败。
这就是尾插的全过程,这里讲的比较详细比较照顾刚入门的同学。
好了代码已经写完了我们现在来测试一下:
现在为大家展示测试结果:
以上就是尾插的全过程。
有了尾插作为基础后面的就很好理解了
我们接下来头插
头插
这里有个100,我们现在要把它通过头插的方式插入到第0的位置。该怎么操作呢?
我们把数据往后挪动,使100可以插入。那么数据的挪动是从前往后,还是从后往前呢?
我们假设一下:从前往后,0挪到1,那么此时1被覆盖很有可能导致数据丢失。
:从后往前,3挪到空位置,然后2挪到原先3的位置,不会使数据丢失。
所以我们使用从后往前进行挪动。
原理很简单,我们来展示代码。
我们来测试一下:
有了这些知识,剩下的就好理解了。
下面把全部代码展示给大家
#define INIT_CAPACITY 4
typedef int SLDataType;
// 动态顺序表 -- 按需申请
typedef struct SeqList
{
SLDataType* a;
int size; // 有效数据个数
int capacity; // 空间容量
}SL;
//初始化和销毁
void SLInit(SL* ps);
void SLDestroy(SL* ps);
void SLPrint(SL* ps);
//扩容
void SLCheckCapacity(SL* ps);
//头部插⼊删除 / 尾部插⼊删除
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
//指定位置之前插⼊/删除数据
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);
int SLFind(SL* ps, SLDataType x);
后续会接着为大家讲解数据结构相关知识
请大家持续关注
感谢你的观看。