目录
- 一、什么是顺序表
- 二、顺序表的分类
- 1、静态顺序表
- 2、动态顺序表(重要)
- 三、C语言实现顺序表
- 1、顺序表的基本结构
- (2)、动态顺序表
- 2、动态顺序表中常见的函数接口
- (1)、初始化
- (2)、销毁函数
- (3)、尾插函数
- (4)、尾删函数
- (5)、头插函数
- (6)、头删函数
- (7)、扩容函数
- (8)、打印顺序表
- 四、顺序表的优缺点
- 1、优点
- 2、 缺点
- 五、测试实验
- 六、关于顺序表的代码实现中的一些细节
一、什么是顺序表
顺序表本质就是一个数组,这个数组可以定长可以不定长,它是指其中的数据是以顺序存储的方式进行存储的,不管在物理上还是逻辑上都是顺序的,在逻辑上是顺序的我们称之为线性表,物理上也是顺序存储的我们称之为顺序表,其存储数据必须由数组下标为0开始进行存储
顺序表的基本模型:
二、顺序表的分类
1、静态顺序表
从上面的图我们可以看出,静态顺序表就是表中的数组是定长数组的顺序表,其大小是不能发生改变的,这种一般不建议进行使用,因为其大小无法满足我们的需求
2、动态顺序表(重要)
动态顺序表是指其中的数据域数组的大小可以发生改变的顺序表,这个一般可以根据具体情况进行扩容
三、C语言实现顺序表
1、顺序表的基本结构
#####(1)、静态顺序表
其一般会配合宏来控制数组的大小,其大小是静态顺序表的一个缺点,大小确定小了,则顺序表不够用,如果确定大了,则可能导致内存的浪费
(2)、动态顺序表
2、动态顺序表中常见的函数接口
所有的函数需要在头文件中进行申明,在对应的源文件中进行定义实现
(1)、初始化
头文件声明:
源文件定义实现:
(2)、销毁函数
头文件申明:
源文件定义:
在顺序表中的销毁函数中,重点是要对资源惊进行释放,因为动态的顺序表的数组是通过malloc或者realloc来申请的,因此需要配套free函数进行资源的释放
(3)、尾插函数
头文件申明:
源文件定义:
基本逻辑:优先考虑是否需要进行扩容,之后再将数据插入到下标为size的位置
(4)、尾删函数
头文件声明:
源文件定义:
基本逻辑:只需要让顺序表标识的有效数据个数-1即可,在进行-1的时候需要检查有效数据个数是否>0,即检查顺序表中是否存在数据可以供删除
(5)、头插函数
头文件声明:
源文件定义:
基本逻辑:检查是否需要进行扩容,挪动数据(从后往前挪),将数据插入到顺序表中
(6)、头删函数
** 基本逻辑:先从第二个位置开始将数据一次往前挪,将要删除的第一个位置的数据进行覆盖,再将顺序表中的有效数据个数-1,进行删除一定要检查顺序表中是否存在数据,即psl->size>0
**
(7)、扩容函数
基本逻辑:先确定扩容后的新容量,我们这里是默认扩成原来的两倍,所以这里需要特别注意,一定要检查原来的容量是否为0,利用realloc函数进行扩容的时候有两种情况(原地扩容和异地扩容),将新申请的空间交给原来顺序表的数据域,更新顺序表中的容量
简单介绍一些realloc函数:
注意:realloc函数在进行扩容的时候会检查原来空间后面的空间内存是否满足新大小,如果满足,则进行原地扩容,如果不满足,则进行异地扩容。异地扩容会将原来空间的数据全部搬运到新空间上,因此此过程的效率会比较低
(8)、打印顺序表
四、顺序表的优缺点
1、优点
在进行数据的查找和尾插尾删的时候可以做到随机访问,时间复杂度为O(1)
2、 缺点
在进行头插和头删的时候需要挪动数据,因此此时效率比较低下,需要进行头插和头删的操作一般不建议使用顺序表
五、测试实验
- 测试尾插和尾删
代码逻辑:
void Test_SeqList1()
{
SeqList sl;
// 这里我们需要注意:当创建一个结构体链表之后需要记得对结构体初始化
SeqListInit(&sl);
// 测试尾插的逻辑
SeqListPushBack(&sl, 1);
SeqListPushBack(&sl, 2);
SeqListPushBack(&sl, 3);
SeqListPushBack(&sl, 4);
SeqListPushBack(&sl, 5);
SeqListPrint(&sl);
// 尾删
SeqListPopBack(&sl);
SeqListPrint(&sl);
SeqListPopBack(&sl);
SeqListPrint(&sl);
SeqListPopBack(&sl);
SeqListPrint(&sl);
SeqListPopBack(&sl);
SeqListPrint(&sl);
SeqListPopBack(&sl);
SeqListPrint(&sl);
}
结果:
- 测试Insert函数接口
代码逻辑:
void Test_SeqList2()
{
SeqList sl;
SeqListInit(&sl);
SeqListPushBack(&sl, 1);
SeqListPushBack(&sl, 2);
SeqListPushBack(&sl, 3);
SeqListPushBack(&sl, 4);
SeqListPrint(&sl);
// 在pos位置插入数据
SeqListInsert(&sl, 2, 5);
SeqListPrint(&sl);
SeqListInsert(&sl, 0, 6);
SeqListPrint(&sl);
SeqListInsert(&sl, 0, 7);
SeqListPrint(&sl);
// SeqListInsert(&sl, 100, 100);// 这个会报错,超出插入的范围
SeqListPrint(&sl);
}
结果:
- 测试头插和头删
代码逻辑:
void Test_SeqList3()
{
SeqList sl;
SeqListInit(&sl);
// 头插
SeqListPushFront(&sl, 1);
SeqListPushFront(&sl, 2);
SeqListPushFront(&sl, 3);
// 3 2 1
SeqListPrint(&sl);
// 头删
SeqListPopFront(&sl);
SeqListPrint(&sl);
SeqListPopFront(&sl);
SeqListPrint(&sl);
SeqListPopFront(&sl);
SeqListPrint(&sl);
}
结果:
六、关于顺序表的代码实现中的一些细节
- 函数接口中传顺序表总是传指针?
如果只是传结构体对象的话,由于两个结构体的对象是处于两个不同的栈帧,因此修改其中一个结构体对象的指不会影响另一个结构体对象的值,传指针的话也能够在很大的程度上减少拷贝 - 在函数的实现中为什么需要对顺序表的指针进行断言检查?
在顺序表中常见的函数接口中,经常需要访问到顺序表中的内容,如果此时用户不小心传了一个空指针进来,如果我们没有对该指针进行断言检查,那么在函数的实现中就会出现对空指针进行解引用(访问空指针指向的空间),此时就会造成程序崩溃 - 在插入函数中需要注意的点
凡是进行插入,一定要提前检查是否需要进行扩容,尾插不需要挪动数据,在其他位置插入都需要挪动数据,在实现在pos位置插入数据的Insert函数接口一定要注意pos为0的情况,此时需要进行特殊处理 - 在删除函数中需要注意的点
凡是进行删除,一定要检查顺序表中是否存在有效数据,即psl->size>0是否成立,如果忽略这一步,就会很容易造成后续的数组越界