用C语言实现一个顺序表(上)
一、准备工作
1.何为顺序表?
顺序表是一种存储数据的方式,是用一段物理地址连续的存储单元依次存储数据的线性结构。我们知道,数组是存储于一个连续空间且具有相同数据类型的元素的集合,因此,我们通常在顺序表中使用数组进行数据的存储。通过对数组的一些操作,完成数据的增、删、查、改等等。
2.实现一个顺序表,我们需要些什么?
(1)数组
经过刚才的分析,数组是不可或缺的了,那么,该给数组开多大的空间呢?有一种叫作静态顺序表,在使用之前开辟好一块固定的空间,但前提是你得知道要存多少个数据。否则开多了浪费空间,开少了又不够使用,它不够灵活。因此,我们往往采用动态顺序表:通过动态内存开辟的方式,让数组的容量跟着需求来。
(2)size和capacity
capacity:表示当前数组能够存多少个数据,指的是数组的容量。(注意:这里的容量不是指数组有多少个字节的空间,而是能够存储数据 的个数,因此,我们可以有这样的公式:数组空间的大小(单位:字节)=数组所存储的单个数据大小 * capacity)
size:表示当前数组已经存了多少个数据。
当 size =capacity 时,我们就该对数组扩容了。
(3)使用一个结构体
我们现在有了一个数组,size(整型)和capacity(浮点型)。它们虽然数据类型不同,但却紧密联系,共同实现一个顺序表,因此,我们可以将他们放入一个结构体中(如图)。
二、顺序表的实现(代码+分析)
1.顺序表的初始化
结构体SeqList告诉我们实现顺序表需要的一些内容,但是它只是一张图纸,就像房屋的设计图一样,告诉我们哪里是客厅,哪里是厨房,却不能实实在在地住进去。因此,我们需要对顺序表进行初始化(代码如下)。
void SeqListInit(SeqList* ps)
{
assert(ps);
ps->a = (DataType*)malloc(sizeof(DataType) * 4);//先开辟4个数据的空间
if (ps->a == NULL)//基本操作,检查动态内存开辟是否成功
{
perror("malloc fail");
return;
}
ps->size = 0;//初始化,还未存入数据,size为0
ps->capacity = 4;//表示初始化后能存4个数据
}
int main()
{
SeqList s;
SeqListInit(&s);
return 0;
}
void SeqListInit(SeqList* ps)是初始化函数,我们发现这里传的是结构体的指针,而不是结构体本身。为什么呢,因为形参只是实参的一份临时拷贝,出了函数的作用域后就销毁了,形参在函数里再怎么变化对实参都没有什么影响,想要改变实参,应当传它的地址。一个经典的例子就是写一个交换a,b值的函数时,传地址才有效,道理是一样的。
2.顺序表的销毁
顺序表的销毁比较简单,代码如下:
void SeqListDestroy(SeqList* ps)
{
assert(ps);
free(ps->a);//释放动态开辟的空间
ps->a = NULL;
ps->size = 0;//存储的有效数据没有了
ps->capacity = 0;//数组的容量也归零
}
3.顺序表的遍历
对顺序表的遍历其实就是对数组的遍历,往往采用一个循环即可。循环的起始就是数组下标为零的地方,而数组现有数据量为size,那么我们也能够轻松地知道循环的终止条件,代码如下:
void SeqListPrint(SeqList* ps)
{
assert(ps);
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
4.顺序表的扩容
动态顺序表的关键特点就是在容量不够时能够进行扩容。
什么时候开始扩容?size和capacity相等时;括多大的空间?根据自己的需求,比如可以两倍扩容;扩容用什么函数?使用realloc函数进行空间大小调整,代码如下:
void CheckCapacity(SeqList* ps)
{
assert(ps);
if (ps->size == ps->capacity)
{
DataType* tmp = (DataType*)realloc(ps->a, sizeof(DataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;//tmp指向的是调整后内存的起始地址
ps->capacity *= 2;//这里采用的是两倍扩容
}
}
5.数序表数据的尾部插入(尾插)
顺序表的尾插,其实就是在数组的末尾插入数据,我们只要找到数组最后一个数据的下标即可。这里需要注意的是,在插入数据之前,应当检查一下数组的空间是否足够,之前的扩容函数就派上了用场,代码如下:
void SeqListPushBack(SeqList* ps,DataType x)
{
assert(ps);
CheckCapacity(ps);//检查是否需要扩容
ps->a[ps->size] = x;//插入数据
ps->size++;//有效数据+1
}
6.顺序表的尾部数据删除(尾删)
顺序表的尾删也相对简单,不过在尾删之前要判断一下顺序表里是否还有数据,代码如下:
void SeqListPopBack(SeqList* ps)
{
if (ps->size == 0)//检查是否存在有效数据
return;
else
ps->size--;//遍历的时候就不会遍历到最后一个元素了
}
7.运行测试
测试代码如下:
int main()
{
SeqList s;
SeqListInit(&s);//初始化
SeqListPushBack(&s, 1);//尾插数据
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPushBack(&s, 5);
SeqListPrint(&s);//打印
SeqListPopBack(&s);
SeqListPopBack(&s);//连续尾删两次
SeqListPrint(&s);
SeqListPopBack(&s);//尾删一次
SeqListPrint(&s);
SeqListPopBack(&s);//尾删一次
SeqListPrint(&s);
SeqListPopBack(&s);//尾删一次
SeqListPrint(&s);//打印
SeqListDestroy(&s);//销毁
return 0;
}
结果如图:
三、本期总结+下期预告
本期内容主要是实现一个顺序表的准备工作+顺序表的一些基本操作的实现,不过这只是其中的一部分,下期内容将为大家介绍顺序表的头插,头删以及某个位置数据的插入删除等等。
感谢大家的关注,我们下期再见!