目录
一、顺序表的概念及结构
要想了解顺序表,首先我们要知道线性表,因为顺序表也是线性表的一种。
线性表:线性表(linear list)是n个具有相同特性的数据元素的有限序列。线性表是⼀种在实际中⼴泛使⽤的数据结构,常⻅的线性表:顺序表、链表、栈、队列、字符串...,线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
二、顺序表的分类
(2.1)静态顺序表
概念:使用定长数组存储元素。
基本定义格式如下:
静态顺序表缺点:空间给少了不够用,给多了造成空间浪费。
(2.2)动态顺序表
动态顺序表底层仍是数组,但不像静态顺序表那样给出指定大小的数组,而是给一个需要存储的数据类型的指针,使用时通过动态开辟内存,并让指针指向动态开辟的空间,前面文章说过,动态开辟的内存是可以通过realloc函数进行扩容的,这样就达到了按需申请的效果,避免了空间不够用,也一定程度上缓解了空间的浪费。
基本定义格式如下:
注意:顺序表和数组的区别在于顺序表的底层结构是数组,对数组进行封装,实现了常用的增删改查等接口。
三、动态顺序表的实现
(3.1)基本结构定义
这里一共创建了三个文件,SeqList.h是用来放头文件,结构体定义,函数声明的;SeqList.c是用来对声明的函数进行定义的;test.c是对写好的动态顺序表进行测试的。
第一部分是在整个顺序表程序中需要用到的头文件(assert.h是为了使用断言 - assert(),assert函数中可以放表达式,若表达式为假,则终止程序并给出错误提示信息,如果想让程序中的断言失效可以定义宏#define NDEBUG)。
第二部分是为了让程序更具有通用性,如果写死指针 a 指向的数据类型,那么当前顺序表就只能应用于这种数据类型,如果想应用于其他数据类型,只能重新写一个顺序表或者对当前顺序表进行彻底的更改,而对当前需要存储的数据类型进行重命名,后面接口都是使用重命名后的名字,这样应用于其他数据类型是只需要将 2 语句中的 int 改成想存储的数据类型就可以了。
第三部分是对动态顺序表结构的定义,一个指向存储数据空间的指针---a,一个记录数组中的有效元素的个数---size,一个记录整个数组的大小(即最多可以存储多少个数据)---capacity。
(3.2)初始化和销毁
(3.2.1)初始化---SLInit
如上图所示,动态顺序表的底层是动态开辟出来的数组,因为这里我们实现的动态顺序表是在第一个数据插入时才为其中的数组开辟空间,而初始化又是对刚创建好的顺序表中的数据进行一个使用前的设置,所以在初始化函数中我们可以先将指针置空,size和capacity置0。
函数使用场景:
在创建好动态顺序表后直接进行初始化。(前面将struct SeqList重命名为了SL)
测试:
(3.2.2)销毁---SLDestroy
使用完动态顺序表后我们需要主动对动态顺序表进行销毁,因为动态顺序表中数组空间是利用动态开辟内存函数在堆上开辟的,系统并不会主动释放,如果我们也不释放,就会造成内存泄漏,所以这里我们在销毁函数中主动通过free函数释放指针指向的空间,并将指针置空,size和capacity置回0。
使用场景:
测试:
(3.2.3)打印函数---SLPrint
因为我们测试时经常需要打印动态顺序表中的数据,所以这里我们直接写一个打印函数方便后续的测试。因为我们前面写的SLDataType代表的是int类型的数据,所以这里的打印函数也是针对int类型的,如果想存储其他类型的数据并使用打印函数进行测试,可以将 printf () 中的%d进行更换。
这里size是动态顺序表中有效元素的个数,而动态顺序表底层是数组,下标是从0开始的,所以最后一个元素下标是size-1,所以循环条件如图中所示。
(3.3)检查扩容
在前面的初始化函数中,我们并没有为数组开辟空间,而是将指向数组的指针置空了,所以后续插入第一个数据时我们一定需要为数组开辟空间,而且如果数据量较大,已经开辟的空间存满了,我们还需要对原空间进行扩容。这里我们通过扩容函数解决这两个问题。
具体实现:
size代表动态顺序表的有效元素个数,capacity代表动态顺序表的总容量,当它们相等时,代表动态顺序表存满了,最开始size = capacity = 0,所以插入第一个数据时也会判断需要扩容,当判断需要扩容后,定义一个newcapacity表示扩容后的容量大小,通过一个三目运算符,如果capacity为0则给4,否则扩容到原空间的二倍(**注意:**capacity不是数组占多少个字节,而是数组最多能存储的当前类型的数据的个数)。然后通过realloc函数对空间进行扩容,这里扩容可能失败,如果用ps中的a指针接收,一旦扩容失败,函数返回空指针,那么连原来的空间也找不到了,所以先定义一个临时的指针,当判断指针不为空(即空间扩容成功)后,在将该地址赋回给管理动态顺序表中数组的指针(即ps中的a),同时更新动态顺序表容量。
(3.4)头部插入删除\尾部插入删除
(3.4.1)尾部插入
要往动态顺序表中插入数据,动态顺序表肯定要存在,所以先断言一下ps不可以为空(这里不一定非要用断言,用 if 语句判断也可以),然后检查一下动态顺序表是否需要扩容,最后将数据插入尾部即可,同时有效数据个数加1(**注意:**size是有效数据个数,所以原有数据的下标是从0到size - 1,所以数据插在尾部就是插在下标为size处)。具体过程可参考下图:
测试:
(3.4.2)尾部删除
只有顺序表存在且不为空的情况下才能进行删除,所以这里先对ps不为空和顺序表中的数据个数size不为0进行断言,然后直接将动态顺序表中的有效数据个数size减1就可以了,没有必要对删除的数据赋特殊值。具体过程可参考下图:
测试:
(3.4.3)头部插入
前面还是一样,断言指向动态顺序表的指针不为空,检查是否扩容,但是接下来如果直接将数据插入头部位置,那么原来的头部位置的数据将被覆盖,所以我们需要移动数据,将数组的起始位置空出来,然后在将数据插入,有效数据个数size加1。具体过程可参考下图:
测试:
(3.4.4)头部删除
只有顺序表存在且不为空的情况下才能进行删除,所以先断言ps指针不为空,size不为0,如果像尾删那样直接进行size-1,删除的是最后一个数据,所以要先进行数据移动,将头部后面的数据都向前移动一个位置,在进行size-1。具体过程可参考下图:
测试:
(3.5)指定位置插入\删除数据
(3.5.1)指定位置插入数据
插入数据首先动态顺序表要存在,其次插入的位置(这里的位置指的是数组下标)要合法,不能为负数,同时最远只能插入到数组最后一个有效数据的后面,所以pos还要小于等于size。这里还是用两个断言来控制这两个问题,然后检查是否需要扩容,在将pos位置及之后的数据向后移动一个位置,将pos位置空出来,最后将数据插入pos位置,并将有效数据个数size加1。具体过程可参考下图:
测试:
(3.5.2)指定位置删除数据
只有顺序表存在且不为空的情况下才能进行删除,并且指定删除的位置要合理,只能在有数据的位置进行删除,这里通过三个断言来控制上述条件,然后把要删除位置后面的数据向前移动一个位置,并将有效数据个数减1。具体过程可参考下图:
测试:
(3.6)查找指定数据
具体实现:
查找数据动态顺序表必须存在,但数据不一定在顺序表中,所以这里只需要断言一下ps不为空就可以,然后就是遍历当前顺序表中的数据,如果找到就返回下标,如果遍历已经结束还没有返回说明顺序表中没有该数据,那么返回-1。
测试:
四、通讯录项目
(4.1)通讯录功能
1)至少能够存储100个⼈的通讯信息
2)能够保存用户信息:名字、性别、年龄、电话、地址等
3)增加联系⼈信息
4)删除指定联系⼈
5)查找制定联系⼈
6)修改指定联系⼈
7)显⽰联系⼈信息
(4.2)动态顺序表的改变
因为通讯录项目存储的是联系人信息,而联系人信息需要用到结构体,所以要想复用前面写好的动态顺序表需要进行一定的改动。
(4.2.1)存储数据类型的改变
这里将SLDataType代表的数据从 int 改为存储联系人信息的结构体PersonInfo。
(4.2.2)打印函数的改变
因为动态顺序表存的是结构体,所以不能直接对顺序表中的数据进行打印,需要像图中一样,先通过数组下标访问到结构体,在访问结构体里的数据。
(4.2.3)查找函数的改变
同理,因为顺序表存储的是结构体,所以比较是否为相同元素时需要比较结构体里的每一个数据,这里存储联系人数据的结构体中的name,sex,tel,adder都是char类型数据的数组,比较是否相同需要用到strcmp函数,age是int类型的,直接比较就可以。
(4.2.4)添加SLSize函数
通过上述函数返回动态顺序表中有效数据个数。
(4.2.5)添加SLAt函数
该函数是用来修改指定位置的值的。在修改值之前要先断言一下顺序表不为空,并且修改位置合法。然后直接将数据放入该位置即可。
(4.3)通讯录基本结构定义
将前面定义好的动态顺序表重命名为contact,定义一个存储联系人信息的结构体,里面包含姓名---name,性别---sex,年龄---age,电话---tel,地址---addr,除了年龄是int类型其余都是char类型的数组,在前面通过宏定义出这些数组的大小。
(4.4)通讯录项目实现的函数
(4.4.1)初始化通讯录
初始化分为两个部分,第一部分是初始化动态顺序表,前面已经讲过了,第二部分是将文件中的数据导入动态顺序表中,如下图:
首先打开存储联系人信息的文件(这个文件需要我们自己创建好),当打开成功后,先创建一个用来存储联系人信息的临时变量,通过fread函数将读到的信息放到临时变量里,再将临时变量的信息放到动态顺序表中,再去读取下一个人的信息,重复上述操作,直到所有联系人信息都被放到动态顺序表中。
(4.4.2)添加通讯录数据
先创建一个临时变量存储要添加的联系人信息,然后通过打印提示信息让用户输入,并将读取到的用户输入放到临时变量中,当所有信息都录入完毕后将信息尾插到动态顺序表中,并提示用户插入成功。
(4.4.3)删除通讯录数据
删除通讯录数据主要通过联系人姓名,当用户输入要删除的联系人姓名后,通过FindByName方法找到要删除的联系人在动态顺序表中存储的位置(即在数组中的下标),如果用户存在,调用顺序表的删除方法将其删除。
FindByName:
在该函数中,我们需要遍历动态顺序表并比较联系人姓名,strcmp函数比较如果相等会返回0,找到后返回下标,否则返回-1。
(4.4.4)展示通讯录数据
该函数只需要遍历打印就可以了,在打印联系人数据前先打印提示信息,使用户知道展示的都是哪些信息。效果如图:
(4.4.5)查找通讯录数据
查找也是通过联系人姓名,当用户输入联系人姓名后通过复用FindByName函数进行查找,如果找到通过返回的下标打印其所有信息。效果如图:
(4.4.6)修改通讯录数据
修改也是通过联系人姓名,用户输入要修改的联系人姓名,复用FindByName函数进行查找,如果要修改的联系人是存在的,打印提示信息并让用户输入直接覆盖原内容。
(4.4.7)销毁通讯录数据
销毁分为两部分,先将动态顺序表中的联系人信息放入文件中,再销毁动态顺序表。SaveContact函数具体实现如下图:
(4.5)测试
测试中写了一个菜单函数,菜单的打印,通讯录的创建,初始化,以及各个接口的调用都在这个函数中完成,这里选择使用do---while循环,因为不管用户想进行何种操作,菜单一定会先打印一次,然后用户选择操作,定义一个变量op接受用户要进行的操作,通过switch---case跳转到相应的接口,输入不合法时会提醒重新输入。