本文讲顺序表之前,先了解一下线性表是个什么样的东西呢?
其实不难理解,线性表顾名思义就是像一根线一样的表,生活中,排队测核酸中一条长长的队伍,一个跟着一个排着队,有一个打头,有一个收尾,如同一根线一样把他们串联在一起。还记得小时候玩过的老鹰捉小鸡的游戏吗,一人当母鸡,一人当老鹰,其余当小鸡,小鸡依次在母鸡后面牵着衣襟排成一队,这就叫做线性表。
线性表主要有两种物理结构:顺序表、链表。而本文是讲顺序表。
顺序表指的是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组的形式存储,在数组上完成增删查改。
顺序表可以分为:
1.静态顺序表
2.动态顺序表
静态顺序表缺点显而易见,只适用于知道需要存储多大数据的情况下。一次性申请一块内存块进行数据存储。如果申请的空间过大,存储的数据又少就会造成很大的空间浪费,如果申请的空间过少呢,要存储较多数据的时候空间反而就会不够用。这时,动态顺序表就能很好的解决这种情况,按需开辟一定量的空间来存储数据,不够用时还能再增容,因此选用动态开辟数组的形式来存储数据较为佳。
接下来我们来实现动态顺序表。
各个接口的实现:
typedef int SLDataType; //数据类型
typedef struct SeqList
{
SLDataType *data; //指向动态开辟的数组
size_t size; //存储数据的有效个数
size_t capacity; //开辟的空间容量
}SeqList;
实现动态顺序表的功能无非就那几个功能:增删查改……
先来实现一下顺序表的初始化
//初始化顺序表
void SeqListInit(SeqList*p)
{
p->data = (SLDataType*)malloc(3 * sizeof(SLDataType)); //开辟能存储3个SLDataType的空间大小
if (p->data == NULL)
{
printf("开辟内存失败\n");
exit(-1);
}
p->size = 0; //把存储个数初始化为0
p->capacity = 3; //容量初始化为3
}
检测容量
拿已经存入的有效个数跟容量比较一下,如果相等的话就证明空间已经用完了就需要扩容。
//检测容量
void CheckCapacity(SeqList*p)
{
assert(p);
//相等就扩容
if (p->size == p->capacity)
{
SLDataType* new = realloc(p->data, (p->capacity + 3)* sizeof(SLDataType));
if (new == NULL)
{
printf("扩容失败\n");
exit(-1);
}
else
{
p->data = new;
//容量增加3个
p->capacity += 3;
}
}
}
顺序表尾插
要知道c语言中的数组是从下标0开始的,要访问数组中的最后一个元素是 i -1。若p是SeqList类型的顺序表,要在尾部插入新的数据,则直接在最后一个元素 i -1 后面插入数据,也就是 i , p->data[ i ] 插入一个元素即可,最后再自增一下已存入元素个数。
//尾插顺序表
void SeqListPushBack(SeqList*p, SLDataType x)
{
assert(p);
//检测容量
CheckCapacity(p);
p->data[p->size] = x;
p->size++; //每存入一个数据就增加一个
}
顺序表尾删
尾删最后一个数据,直接size--就可以了,size是存入数据的有效个数,size--了就访问不到最后一个元素了。但是我们要注意一点,要加个判断size是否为0了,如果没有加这个判断条件的话就会一直减下去导致出现问题。
void SeqListPopBack(SeqList*p)
{
assert(p);
//顺序表为空的话就返回,不为空则size--
if (p->size == 0)
{
printf("该顺序表为空,无法再进行删除\n");
return;
}
else
{
p->size--;
}
}
顺序表头插
想要在前面插入数据,就需要先把数据整体往后移,给前面空出一个空间来存放数据。
举个例子:我们在学校食堂里排队打饭时,同学们都很自觉的排好队伍,突然不知从哪杀出一个程咬金,素质感人的说着:大哥,求求你帮帮忙,我已经七七四十九天没有吃过饭了,快饿得撑不住了,队伍这么长可以让我排在你前面不?让我吃口饭,这时要是你于心不忍的话就会同意了,你就得往后退一步,否则他就没法插进队伍来,这可不得了,你往后退了一步,后面的人也都跟着退了一步。顿时,骂声四起,也无可奈何。
//头插顺序表
void SeqListPushFront(SeqList*p, SLDataType x)
{
assert(p);
//检测容量
CheckCapacity(p);
//依次递减的把数据往后挪动
for (int i = p->size; i > 0; i--)
{
p->data[i] = p->data[i - 1];
}
//前面空出一个位置后插入数据
p->data[0] = x;
//有效个数自增
p->size++;
}
顺序表头删
头删一个数据跟上面的头插一样的道理,也是得整体移动数据,整体移动数据来覆盖前一个元素以此来达到头删的目的。
接着刚才的例子:因为大家都肚子饿嘛已经等了许久了意见都很大,后面队伍中有个大个子壮汉实在忍不了了:不管什么原因,插队这种行为就是不行,有本事自己做饭去。说罢,一只手就把这个插队的小伙子拎起来甩出队伍。这个小伙子似乎是意识到挺羞愧的,捂着脸,翘着臀,一扭一扭地往着外边小跑出去。于是,队伍中人们又整体的往前挪动了一步,骂声渐息,队伍恢复了平静。
//头删顺序表
void SeqListPopFront(SeqList*p)
{
assert(p);
//如果顺序表为空则返回
if (p->size == 0)
{
printf("该顺序表为空,无法再进行删除\n");
return;
}
//顺序表不为空,往前移动
for (size_t i = 0; i < p->size - 1; i++)
{
p->data[i] = p->data[i + 1];
}
//因头删一个数据后需自减
p->size--;
}
顺序表查找
一个一个的遍历数组,找到想要查找的数值就返回该数值的下标,如果遍历完数组也没有找到要查找的数值的话返回-1。
//查找顺序表
int SeqListFind(SeqList*p, SLDataType x)
{
assert(p);
//遍历数组
for (size_t i = 0; i < p->size; i++)
{
//找到就返回该下标
if (p->data[i] == x)
{
return i;
}
}
return -1;
}
顺序表pos位置插入x
pos位置插入x跟头插一样也是需要把数据整体往后移动。但需要注意加个判断条件来判断pos是否存在越界。
//pos位置插入数据
void SeqListInsert(SeqList*p, size_t pos, SLDataType x)
{
assert(p);
//如果pos位置越界则返回,相反则插入x
if (pos > p->size )
{
printf("要插入的位置越界了\n");
return;
}
else
{
//检测容量
CheckCapacity(p);
//整体往后挪动
for (size_t i = p->size; i > pos; i--)
{
p->data[i] = p->data[i - 1];
}
p->data[pos] = x;
p->size++;
}
}
删除pos位置的值
//删除pos位置元素
void SeqListErase(SeqList*p,size_t pos)
{
assert(p);
//顺序表为空返回
if (p->size == 0)
{
printf("该顺序表为空,无法再进行删除\n");
return;
}
//pos位置越界则返回
if (pos > p->size )
{
printf("要删除的pos位置越界\n");
return;
}
else
{
//数据整体往前移动来覆盖要删除的元素
for (size_t i = pos; i < p->size - 1; i++)
{
p->data[i] = p->data[i + 1];
}
p->size--;
}
}
顺序表销毁
//顺序表销毁
void SeqListDestory(SeqList* p)
{
assert(p);
free(p);
p->data = NULL;
p->capacity = 0;
p->size = 0;
}
打印顺序表
//打印顺序表
void SeqListPrint(SeqList*p)
{
assert(p);
if (p->size == 0)
{
printf("顺序表为空\n");
return;
}
for (size_t i = 0; i < p->size; i++)
{
printf("%d ", p->data[i]);
}
printf("\n");
}
顺序表的优缺点:
优点:1、无需为表示表中元素之间的逻辑关系而增加额外的存储空间。
2、可以快速地存取表中任一位置的元素。
缺点:1、插入和删除操作需要移动大量元素。
2、当线性表长度变化过大时,难以确定存储空间的容量。
3、造成存储空间的碎片。
那么这些缺点是能解决的吗?下回见分晓!!!