本章要分享到内容是数据结构线性表的内容,那么学习他的主要内容就是对数据的增删查改的操作。
以下为目录方便阅读
目录
1.线性表中的顺序表和顺序表
2.顺序表
2.1概念和结构
2.2动态顺序表使用场景
比如我们看到的所显示出来的群成员的列表这样所展示出来的数据。
同时呢我们也可以对里面的成员内容进行操作
右击一个成员你就可以看到一些可以操作的内容,比如移除群聊;当然我们如果想邀请别人加入群聊的话也是对这个表里的内容进行操作,同样的输入关键字也可以找到这个表中对应的内容所以这就是数据在我们以后的业务中涉及到的一些增删查改的问题。
1.线性表中的顺序表和顺序表
首先要说的是顺序表,顺序表的本质上就是数组,怎么就是数组了呢?
给大家上图演示一下
竖着可能不太好理解,但是这样横着放应该比较容容易理解一些,这样就和我们平时学习的数组没什么区别。
但是数组是有一些缺陷的, 比如说我想将上面的其中一个成员删掉,用数组来做还是有点困难的。在我们平时的学习中,在数组中删除掉其中一个数据,也可以理解为释放掉数组中的一块空间是做不到的,释放空间只能释放掉这个整体,所以顺序表想要做到增删是非常困难的。
我们想要删除内容的本质是挪动数据覆盖,也就是说将后面的数据向前一个一个的挪动,将想要删除的人的数据覆盖掉,就相当于完成了删除的操作。
那数组在增删这方面是有缺陷的,有没有什么好的方法可以弥补数组的缺陷呢?
所以有一种存储方式叫做链表,链表就是一小块一小块的空间,不是一个完整的连续的空间。
相对于顺序表中指针指向第一块空间,就可以找到后面所有的空间,链表却做不到这些。
因为链表中的空间是一块一块的,是多次使用malloc函数开辟出来的,他们的地址之间没有关联。
那地址之间没有关系是怎么连接起来的呢?
链表通过下一个空间内容中存放上一个空间的指针,下一个空间内容中存放上一个空间的指针,循环往复就将一小个空间连接了起来
假如我们想要将上图中第二个内容删除,我们可以直接free释放掉其内容(空间是malloc出来的),然后让第三个空间存放第一个空间的地址即可(因为第二个空间的内容已经被删除掉,无需再存放其地址),无需 再挪动数据。
所以用数组挪动数据就非常的费劲,使用链表就会轻松很多。
那既然数组增删时这么难受为什么还要学习数组呢?
虽然数组再增删时非常的费劲,但是数组的好处就是可以随机访问下标, 可以随机访问下标的好处就是可以使用二分查找以及其他的查找方法查找数据,反观二分查找却不能在链表上使用,因为链表的地址时不连续的。
所以这就是我们为什么要学习数据结构,不同的储存方式有不同的使用场景,根本不存在一种结构就可以包含各种结构的优点,了解得更多我们便可以在以后的业务中使用的得心应手。
接下来要更深入的了解顺序表表
2.顺序表
2.1概念和结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存
储。在数组上完成数据的增删查改。其实顺序表对数组还是有要求的,不是在数组中随意存储,必须是连续存储。
数据表一般分为两种:静态数据表和动态数据表
静态顺序表
#define N 10
typedef int SLDatatype;
struct Seqlist
{
SLDatatype a[N];
int size;//存储的有效数据个数
};
以上就是一个简单的静态顺序表,为了方便修改数组空间我们不妨使用宏定义;为了方便修改数据我们不妨用typedef替换,应该不难看懂。
静态顺序表的缺点就是它的空间时固定的,我们如果想存放十一个数据就会无法存储,也就是说在存储数据时,空间给小了不够用,给多了又用不上,达不到我们想要的效果,在业务中也很少使用。
动态顺序表
//动态顺序表
typedef int SLDatatype;
struct Seqlist
{
SLDatatype* a;// 指向动态开辟的数组
int size;//存放数据的有效个数
int capacity;//容量
};
和静态顺序表稍微有些不同,动态顺序表要使用malloc开辟空间。定义了一个指针让其指向动态开辟的数组。
2.2动态顺序表使用场景
接下来实现两个简单的使用场景,分别是初始化和释放空间。
void SLInit(SL sl);
void SLDestory(SL sl);
我们简单的实现一下这两个函数。
首先是初始化函数
void SLInit(SL sl)
{
sl.a = NULL;
sl.capacity = 0;
sl.size = 0;
}
这里采用的方式是手动的将其内容都将成员初始化成0。
然后再用main函数调用函数,代码如下
#include"Seqlist.h"
int main()
{
SL s;
s.a = NULL;
SLInit(s);
return 0;
}
但是我们要注意的是,不可以直接再SLInit中的参数直接传值过去
我们调试观察一下
我们观察到虽然我们调用了SLInit自定义初始化函数来初始化结构体s,但是我们发现s中的值并没有像sl中的值被初始化,s中的值还是一堆乱糟糟的。
所以以上错误例子指出我们要在给结构体传参的时候要传地址,而不是传值 。
那接下来我们使用传址的方式看能不能初始化结构体s;
int main()
{
SL s;
s.a = NULL;
SLInit(&s);
return 0;
}
只需要给参数的前面加上&,就会结构体s的地址
同样的,函数中的参数也需要变成一个指针类型来接收这个参数,当然内容也要从改变值变为改变成员的地址,以下是修改后的的SLInit
void SLInit(SL* psl)
{
psl->a = (SLDatatype*)malloc(sizeof(SLDatatype)*4);
if (psl->a == NULL)
{
perror("malloc fail");
return;
}
psl->capacity = 4;
psl->size = 0;
}
可以看出我们使用了指针变量来初始化这个结构体的成员变量,注意也不要忘记开辟空间时要判断开辟空间是否成功。
那么SLDestory的实现就非常简单,我们只需要将使用过的空间释放掉就好了,代码如下
void SLDestory(SL* psl)
{
free(psl->a);
psl->a = NULL;
psl->size = 0;
psl->capacity = 0;
}
。
接下来加一点难度,我们使用顺序表来在顺序表的末尾添加数据,我们不妨定义一个函数名为SLPushBack的函数来解决这样的问题。
在写函数内容之前我们要明确思路怎么样才能实现尾部插入呢?
观察下图你就会发现
siz在上图中的意义是有效的数据个数,也可以理解成最后一个数据,我们只要通过访问size的下标就能知道最后一位在哪儿,所以我想插入数据只用在size的位置插入即可。
下面用代码来实现
void SLPushback(SL* psl, SLDatatype x)
{
psl->a[psl->size] = x;
psl->size++;
}
参数有两个,第一个是我们想要操作的结构体,第二个参数就是我们想要加入的数字;
根据上图的描述我们知道psl要指向在a数组中size所在的位置,添加完后继续访问后面的空间来插入数据。
但是上图中我们发现最多再添加两个数据这个顺序表的空间就满了,所以我们不妨再检查一下这个数据表的容量。考虑到以后还要使用到检查空间,我我们不妨将检查空间这个步骤写成一个函数CheckCapacity,一劳永逸。
void SLCheckCapacity(SL* psl)
{
if (psl->size == psl->capacity)
{
SLDatatype* tmp = (SLDatatype*)realloc(psl->a, sizeof(SLDatatype) * psl->capacity * 2);
if (tmp == NULL)
{
perror("relloc fail");
return;
}
psl->a = tmp;
psl->capacity *= 2;
}
}
既然要扩容,我们就得先判断一下原先的空间是否被塞满,不妨使用if语句,判断有效数据个数和容量空间的大小是否相等,如果相等的话就得使用realloc来调整所开辟的空间的大小,最好是开辟原本空间两倍的大小,以防止小了不够用,大了用不上的问题,同样的还要判断开辟空间是否成功。
如果开辟空间成功我们就将新空间tmp重新赋给a
这就是检查数据表中的容量。
我们不妨将整个过程验证一下,将输入的数字都打印出来,不妨写一个打印函数来遍历我们的数据
void SLPrint(SL* psl)
{
for (int i = 0; i < psl->size; i++)
{
printf("%d ", psl->a[i]);
}
}
同时我们要在main函数中添加数据
#include"Seqlist.h"
int main()
{
SL s;
s.a = NULL;
SLInit(&s);
SLPushback(&s, 1);
SLPushback(&s, 2);
SLPushback(&s, 3);
SLPushback(&s, 4);
SLPushback(&s, 5);
SLPushback(&s, 6);
SLPrint(&s);
SLDestory(&s);
return 0;
}
可以看到我们上面所写的函数都发挥了作用,我们刚开始只有四个空间,那么就是说第五个数据就会扩容
以上就是顺序表的简单的使用,如果对你有帮助,还请三连支持,感谢您的阅读