一、线性表
概念:
是N个具有相同特征的数据元素的有限序列,线性表在逻辑结构上是一定连续的,在物理结构上就不一定是连续的。
那么物理结构和逻辑结构是个啥东西呢?
物理结构:
在存储数据时在内存中存储的真实的位置,物理结构连续就是在内存中使用的是一块连续的内存存储的,比如说我们的数组,其开辟的内存空间就是一块连续不断的。
逻辑结构:
其说的是数据元素之间的关系,逻辑结构连续就是可能其物理结构是不连续的,但是我们通过某种算法啥的,使得这些数据元素可以是连续的,这就是逻辑结构连续。
当线性表的物理结构也是连续的话,那么一般就是使用的数组来存储,当线性表物理结构不是连续的时候,那么一般就是以链式的结构存储的,线性表在实际中广泛使用的一种数据结构,常见的有:顺序表、链表、栈、队列、字符串......
二、顺序表
1、概念与结构
概念:
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。

那么我们为啥不直接使用数组呢?
数组就只能实现连续存储数据,但是顺序表,其不仅仅可以存储数据,还可以对数据进行一系列的操作:增删改查等。
所以单独一个数组并不能实现这些功能,我们实际上是使用结构体来实现顺序表,对于不同的顺序表其结构体的定义也是有区别的,那么就是说顺序表也还有多种。
2、顺序表的分类
顺序表可以分为:静态顺序表、动态顺序表。
静态顺序表:
静态顺序表是指其底层的数组的大小是静态的,就是说其数组的大小在定义的时候就定了,在后续是无法进行变化的。
其定义如下:

上面我们说过了,顺序表实际上是使用一个结构体实现的,静态顺序表中,其表的大小是确定了的,那么我们使用#deffine来定义一个常量来确定其大小。
但是我们对于其存储的数据的类型是不确定的,那么我们可以使用typedef关键字来对数据类型进行命名,那么当我们需要存储啥类型就对其进行修改即可。不再需要对顺序表进行修改。
然后我们这个结构体的命名也是讲究的,其实际上是由两个单词组成的:SepList是由sequential,其意思是顺序,List就是表的意思,那么合在一起就是顺序表的意思。
下面我们再分析这个结构体的成员:
在静态顺序表中有两个成员,一个是底层存储数据的数组,其大小是定的,第二个成员是一个整型变量size,其用处是目前顺序表中有效的数据个数。
静态顺序表其缺点相信大家也可以猜到:
其大小是固定的,那么我们可能会使得给的空间太大然后造成空间的浪费,那么有的同学就会说了,现在内存又不贵,浪费一点点也没啥啦,但是一个项目的人员是很多的,那么每个人都浪费一点,那么加起来就很多了。
但是空间给小了又会不够用,相信我们很多人都网购过,对方在后台可以看到我们的订单信息,那么如果我们的表给小了,没又将这些信息存储进来,造成了用户流失,这对于公司来说将会是一笔很大的顺损失,可能我们就要喜提"编制"了。
那么有没有一种表可以根据实际需求来确定表的大小呢?、
那么就是我们下面要讲的动态顺序表了。
动态顺序表:
在上面的静态顺序表中其大小一旦确定是无法进行修改的,其静态指的就是其大小是固定的。那么动态顺序表就指的是其大小是可变的。
比如我们现在只有四个人,但是现在又加进来了四个,那么此时空间就不够了,那么我们此时程序识别到原来的空间不够了,那么其此时就会先进行扩容,然后再将这加进来的四个人存储进去。
其定义如下:

在动态顺序表中,数组的大小是通过动态内存来开辟申请空间的,当其空间不够的时候,其就会进行空间的扩容,所以其第一个成员是一个指针,其指向的是开辟的这个空间的首地址,第二个成员就和我们的静态顺序表一样,表示当前的表中的有效元素个数,第三个成员就表示的是当前这个顺序表的容量,不过其不是表示字节数,而是表示这个表可以存储的元素个数。
当表示当前表中有效元素个数的size和表示当前表的最大容量的capacity一样的时候,那么此时就表示这个表的空间已经满了,那么此时我们就该进行增容了。
通过上面的学习我们就可以看到其和静态顺序表对比其优点如下:
我们的空间的浪费可以减小,然后也不怕空间给小了不够用,所以其使用起来更加的灵活,所以我们日常中更多的也是使用动态顺序表。
注意:
如果没有加多说明,顺序表一般默认是动态顺序表。
三、顺序表的实现
1、顺序表的定义
在上面我们提到了动态顺序表,其有三个成员,第一个成员是一个指针,用来指向开辟的空间,然后第二个成员就是用来记录当前表中的有效元素的个数,第三个成员就表示当前表的容量。
其定义如下:
、
然后我们就使用我们的动态内存管理函数来开辟空间,如果忘记的可以往前复习一下,下面我们简单进行复习一下。
malloc函数,其是可以申请一块连续的空间,返回的是这块空间的地址。
calloc函数,其和上面的函数是一样的功能,就是其参数不一样,第一个参数是要开辟的元素个数,然后第二个参数是开辟的一个元素的大小。然后其还会将这些地址的内容初始化为0。
realloc函数,这个函数可以对前面两个函数申请的空间进行扩容。
然后我们这个顺序表的名字因为我们需要经常使用带到,所以我们和数据类型一样,使用typedef来给这个结构体重新命名为SL那么我们后续对于顺序表就使用SL即可。
2、顺序表的初始化:
我们创建好一个顺序表后,那么我们需要对其进行初始化后再进行使用,因为一开始顺序表是啥也没有的,所以其结构体中指针指向为空,然后记录数组表中有效元素为0,容量也为0。
不过要注意的是,只有在地址传递才可以改变实参的值,值传递是没办法改变实参的值的。
所以我们初始化函数要传入的数据类型应该为SL*,初始化函数实现如下:

3、顺序表的销毁
我们上面的顺序表的内存是动态申请的,所以我们使用完这个顺序表后是需要对其进行销毁的,将这个空间进行释放,防止内存的泄露,释放完后我们的数组指针置为空,然后另外两个成员重新为0。
然后这个函数的参数还是这个结构体指针,不过有个特殊情况就是,如果传入的这个表本身就是空的,那么我们就不需要再进行销毁了,因为其本来就不存在的,我们也无从下手。
函数的实现如下:

我们要注意的是里面的指针指向的空间被释放后,我们要将这个指针变量置为空指针,不然其就成为野指针了。
4、顺序表的增容
我们在对顺序表进行存入数据的时候,就可能会出现容量不够的情况,那么此时我们就需要进行增容,所以增容在动态顺序表是一件要经常做的事情,所以我们写一个函数来实现顺序表的增容。
然后我们的增多少呢?前面我们在复杂度的时候讲过了,一般增大到当前容量的两倍,那么我们如何判断顺序表的容量是否已经满了呢?我们前面在定义顺序表的时候,第二个和第三个成员,前者是记录当前顺序表的有效元素个数,后者是记录当前顺序表的容量,那么就是说当顺序表的有效元素个数和容量一样的时候就代表顺序表已经满了,那么此时就需要进行增容了。
还有个问题就是我们顺序表初始化函数中,给表示当前容量的变量capacity赋值为0,那么其乘于任何数都为0,所以我们使用一个三目表达式,判断当前的capacity是否为0,如果为0,那么就给其赋值个4 ,如果不是0,那么就对其进行二倍增容。
因为扩容是对顺序表的内存进行修改,那么我们还是需要传入这个顺序表的地址。
函数实现如下:

5、顺序表的插入
我们创建好顺序表后,对其进行初始化后,那么我们就要对其进行插入数据了,我们对于数据的插入主要有三种方式插入:头插、尾插、指定位置插入。
头插就是从顺序表的开头进行数据的插入,尾插就是从顺序表的尾部进行插入数据,指定位置就是通过传入的参数确定要插入的位置。
下面我们对这三种插入方式细讲:
尾插:
尾插,主要就是从尾部进行插入,那么就是项4这个数组的屁股进行插入,那么我们只需要找到这个数组的最后一个位置即可,那么我们的size就刚刚好合适,其表示的是数组的有效个数,那么我size此时当作下标就是数组的最后一个位置的下一个位置。
那么我们只需要使用size为下标,就可以实现尾插了,但是我们在一个顺序表插入数据的前提是这个顺序表的容量还足够,那么我们需要先对其进行判断,那么我们此时就可以使用我们上面的增容函数来判断其空间是否已满。
由于我们此次还是需要对顺序表进行修改,那么我们还是需要传入顺序表的地址。
函数实现如下:

要注意的是我们插入一个数据后,要对size进行+1操作,因为此时的顺序表的有效数据的数量+1了。
头插:
头插就是从顺序表的开头进行数据的插入,那么此时该如何插入呢?
例如,我们有一个顺序表如下:

那么我们插入x,那么我们需要将原来的数据先往后挪一位才行,那么我们要怎么移动呢?是从前面开始移,还是从后面还是往前移动呢?
如果是从前面开始移动,那么我们就会将第二个位置的元素给覆盖了,那么就会造成顺序表的内容被修改了,所以我们是从后面开始移动,那么我们该如何移动呢?
可以发现,我们将下标为size-1的数据赋值给size即可,但是我们不可以将size进行--,因为这样到最后没办法准确的记录顺序表的有效元素个数,所以我们使用一个for循环使i=size,然后进行移动,当第一个位置的数据移动到第二个位置的时候,那么移位也就完成了,那么此时i就刚好为1,那么循环的条件我们设置为i>=1。
当移位完成后就可以进行插入数据了,然后size进行+1操作。
还有就是和上面尾插一样,插入前,我们先判断顺序表的容量是否够。
函数如下:

指定位置的插入:
这个和上面的两种插入方式就有很大的不同了,其是根据用传入的参数来指定位置插入,这个位置一般是这个数组的下标,那么我们对于这个下标也是有要求的,其范围为:>=0和<=size。
这是为啥呢?
当其指定的位置小于0的时候,那么此时就会越界访问了,当其指定的位置大于size的时候,那么就会造成顺序表不连续了。其实两个端点是两个比较极端的情况,当其指定的位置是0的时候,其实就是我们的头插,当其是size的时候,其实就是尾插。
那么我们指定位置的插入该如何进行插入呢?
例如我们有下面的一个表:

此时我们要在下标为2的位置插入一个数据99,那么我们该如何进行插入呢?
那么我们可以先将下标为2及其后面的元素都往后移动先,然后再将数据进行插入。
那么我们和前面的头插类似,从后面开始移动。我们还是使用一个for循环,然后使i=size,然后将其往后移动,当i=指定位置的下标+1的时候就完成了移动。
和上面一样,我们下对传入的地址是否有效,然后对这个顺序表的容量是否足够。然后我们判断这个指定的位置是否符合要求,符合要求的才可以进行插入。
函数如下:

6、顺序表的删除
前面我们实现了对顺序表进行数据的插入,那么有插入就有删除,那么删除的方式也是有三种:头删、尾删、指定位置删除。
头删就是删除顺序表的第一个元素,尾删就是删除顺序表的最后一个元素,指定位置删除就是对于传入的位置进行删除,不管是那种删除的方式,我们都要注意的是,这个表是要有数据的,不能是个空表。
尾删:
尾删就是删除这个顺序表的最后一个元素,那么我们就需要找到这个位置,就是在size-1的这个下标的位置。
那么我们要如何进行删除呢?是对这个位置的元素置0吗?其实都不是,我们前面有讲到,我们size是用来记录这个顺序表的有效元素个数的,那么我们的size-1后,是否就代表我们的顺序表的元素数量减少了一个,那么其去掉的其实就是最后一个元素。
此时也不会影响我们的插入,因为我们插入的话就直接进行覆盖了。
不过要注意的是我们要保证这个表是一个有内容的表,也就是其有效元素个数是大于0的,也就是size是大于0的。
函数实现如下:

头删:
头删,顾名思义就是删除开头的元素,那么我们是如何删除呢?我们要记得一点我们删除后,是要将后面的元素往前移动的,那么我们直接将后面的元素往前移动覆盖住其不就可以达到我们想要的效果了吗,然后对size-1操作,但是有细心的同学就会发现,我们移动完后,其实下标为size的位置的话是有元素的,但是这其实不影响,这是因为我们size进行-1操作后,我们实际的有效元素个数是不包括size这个下标位置的元素的,我们此时再进行插入操作也是很顺利的。
那么我们该如何将元素都往前移动呢?
其实也很简单,我们只需要从头开始,将后一个位置的元素覆盖掉前一个位置的元素即可,然后当其移动了size-1次就完成了。
函数如下:

指定位置删除:
指定位置删除数据,和我们前面的的指定位置插入数据很类似,不过其参数比插入数据少了要插入的数据,就只需要删除的顺序表和要删除的下标,不过要注意的是这个下标要符合>=0和<=size-1。
那么我们要如何进行删除呢?这里就和头删有点类似,就是将这个位置后面的数据往前移动,将这个位置的元素覆盖即可,那么我们需要移动多少个元素呢?
就是size-1-pos个元素,我们可以自己举个例子来验证。
然后移动完后不要忘记对size-1操作。还有不要忘记先对size进行判断其大于0,也就是判断这个表是否为空表。
函数实现如下:

7、顺序表的查找
顺序表的查找就是根据给定的值,然后我们去顺序表中查找其是否在表中,如果不在就返回-1,如果存在就返回其下标,那么我们就只需要遍历数组即可实现。
函数如下:

8、测试函数:
尾插函数的测试:

头插函数的测试:

指定位置进行数据插入:

尾删:

头删:

指定位置删除:
顺序表查找:
到这里我们的顺序表的内容就讲完了,下面我们大家就先去实现顺序表的增删改查的功能吧,上面的实现方法肯定不止一种,要是你们有其他方法可以一起学习交流喔。