概念
在写顺序表之前,我们需要了解什么是顺序表。
顺序表是数据结构中的一种基本线性表存储方式,他通过一组连续的内存地址存储数据元素。并且按照它们在表中的逻辑顺序依次排列。
在顺序表中,第 i 个元素的实际存储位置与第 i+1 个元素相邻(不考虑任何额外的存储需求,如头结点等)
特点
从上面几段我们可以了解到,顺序表的几大特点:
- 元素存储在了连续的内存空间中。
- 每个元素都有一个确定的位置(索引)。
- 访问特定位置的元素非常快。
- 插入和删除操作可能涉及大量的元素移动。
结构体的实现
了解完特点后,我们将通过C语言来实现顺序表
首先,我们要实现一个顺序表,要先实现它的结构体,结构体内要有它的"身份信息"
struct SeqList
{
int* a;
int size;
int capacity;
}SL;
我们从这些了解到,在顺序表的结构中,有三个属性。
- int* a : 表示顺序表的数据存储区域,指向起始地址。
- int size: 表示顺序表中当前的有效数据数量,即实际存储了多少个元素。
- int capacity:表示顺序表的最大容量,即分配给顺序表的内存空间能够存储的最大元素数量。
这三个属性就是顺序表的基础结构,以此作为延伸,为它提供增加,删除,查询,更新操作。
结构体的优化
在添加新的操作之前,我们先优化上面的结构体,我们会发现,我们现在写的顺序表,是只适用于整型的,我们需要通过修改或者添加哪个关键字可以让这个顺序表变成一个易于修改适用类型的结构呢?
我想聪明的读者们肯定想到了C语言中的一个关键字 - - - [typedef]
其简单的作用就是可以给别的数据类型起一个别名
typedef int SLDataType;//起个别名,方便以后改
typedef struct SeqList
{
SLDatayType* a;//目前是int* a
int size;//有效数据
int capacity;//空间容量
}SL;
我们改完这个结构体后,就可以开始正式的编写我们的顺序表了。
需要创建三个文件,分别是:
- SeqList.h - - - 存放函数声明
- SeqList.c - - - 编写函数具体操作
- Test_Main.c - - - 测试入口
先在SeqList.h文件中放入结构体 并且 明确顺序表要有的函数:
//初始化
void SLInit(SL* psl);
//销毁列表
void SLDestroy(SL* psl);
//检查容量,是否需要扩容
void SLCheckCapacity(SL* psl)
//插入操作
//尾插
void SLPushBack(SL* psl,SLDataType x);
//头插
void SLPushFront(SL* psl,SLDataType x);
//任意下标位置插入
void SLInsert(SL* psl,int pos,SLDataType x);
//删除操作
//尾删
void SLPopBack(SL* psl);
//头删
void SLPopFront(SL* psl);
//任意下标位置删除
void SLErase(SL* psl,int pos);
//找寻下标,没找到返回-1
int SLFind(SL* psl,SLDataType x);
//打印
void SLPrint(SL* psl);
再去Test_Main.c测试文件中实例化一个结构体,方便后续测试:
int main()
{
SL s;//实例化
SLInit(&s);//把s传入初始化函数
}
我们现在按照SeqList.h的顺序依次去实现这些函数:
初始化,销毁与扩容函数
初始化
为了什么?
- 避免不确定状态
- 提高程序可靠性
- 简化调试过程
void SLInit(SL* psl)
{
assert(psl);
psl->a = NULL;//将指针a指向空NULL
psl->size = 0;//数组大小为0 代表无元素
psl->capacity = 0;//容量为0 代表无法存储元素
}
销毁
目的是什么?
- 释放内存
- 清理资源
- 防止内存泄漏
void SLDestory(SL* psl)
{
if(psl->a != NULL)//当psl
{
free(psl->a);//指针a需要释放
sl->a = NULL;//将指针a置为空
ps->size = psl->capacity = 0;//元素个数与容量置为0
}
}
扩容
void SLCheckCapacity(SL* psl)
{
if(psl->size == psl->capacity)//当数组内元素 与 最大容量相同时
{
int NewCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;//新的容量
//开个新的空间
SLDataType* tmp = (SLDataType*)realloc(psl->a,sizeof(SLDataType)*newCapacity);
if(tmp == NULL)
{
perror("realloc fail");
return;
}
//将开辟出来的新空间 给指针a
psl->a = tmp;
//旧空间被替换成新空间
psl->capacity = newCapacity;
}
}
插入部分
头插:
从表的开头插入元素,通过SLCheckCapacity函数先完成对容量的检查,检查完毕后,我们先记录尾部的位置,从前向后去挪动数据,将前面的数据往后挪,使得头部空出一个位置,再把头部的位置赋值即可。
void SLPushFront(SL* psl,SLDataType x)
{
assert(psl);
SLCheckCapacity(psl);
//挪动数据
int end = psl->size - 1;//记录尾部
while(end >= 0)
{
psl->a[end+1] = psl->a[end];//挪动前面的数据
--end;
}
psl->a[0] = x;
psl->size++;
}
尾插:
从表的尾部插入元素
void SLPushBack(SL* psl,SLDataType x)
{
assert(psl);
SLCheckCapacity(psl);
psl->a[psl->size] = x;//psl->size - 1 是最后一个元素,size表下标时,代表在原先的数组外
psl->size++;//扩充有效元素个数
}
任意位置插入:
可以从pos位置插入
//注意pos是下标
//size是数据个数,看做下标的话,他是最后一个数据的下一个位置
void SLInsert(SL* psl,int pos,SLDataType x)
{
assert(psl);
assert(pos>=0 && pos <= psl->size);
SLCheckCapacity(psl);
//挪动数据
int end = psl->size - 1;
while(end >= pos)
{
psl->a[end+1] = psl->a[end];
--end;
}
psl->a[pos] = x;
psl->size++;
}
删除部分
头删:
void SLPopFront(SL* psl)
{
assert(psl);
assert(psl->size > 0);
int begin = 1;//记录第二个元素位置,方便由前向后挪动数据
while(begin < psl->size)
{
psl->a[begin - 1] = psl->a[begin];//让后面的数据覆盖前面的
++begin;
}
psl->size--;//将最后一个多出来的元素删除
}
尾删:
void SLPopBack(SL* psl)
{
assert(psl);
assert(psl->size > 0);
psl->size--;//直接减少有效的元素个数
}
任意位置删除:
void SLErase(SL* psl,int pos)
{
assert(psl);
//删除的时候不能等于size
assert(pos>=0&& pos < psl->size);
//挪动覆盖
int begin = pos + 1;
while(begin < psl->size)
{
psl->a[begin-1] = psl->a[begin];
begin++;
}
psl->size--;
}
打印与查询
打印:
void SLPrint(SL* psl)
{
for(int i = 0;i<psl->size-;++i)
{
printf("%d ",psl->a[i]);
}
printf("\n");
}
查询:
int SLFind(SL* psl,SLDataType x)
{
assert(psl);
for(int i = 0; i < psl->size; ++i)
{
if(psl->a[i] == x)
{
return i;
}
}
return -1;
}
最后我们补齐测试代码:
void menu()
{
printf("*****************************************");
printf("1,尾插数据 2,尾删数据\n");
printf("3,头插数据 4,头删数据\n");
printf("5,打印数据 0,退出");
printf("*****************************************");
}
int main()
{
SL s;
SLInit(&s);
int option = 0;
do{
menu();
printf("请输入你的选择:>");
scanf("%d",&option);
if(option == 1)
{
/*printf("请输入你要尾插的数据,以-1结束:>");
int x = 0;
scanf("%d",&x);
while(x != -1)
{
SLPushBack(&s,x);
scanf("%d",&x);
}*/
printf("请依次输入你要尾插的数据个数和数据:>");
int n = 0;
scanf("%d",&n);
for(int i = 0;i < n; ++i)
{
int x = 0;
scanf("%d",&x);
SLPushBack(&s,x);
}
}
else if(option == 2)
{
printf("删除尾部数据:>");
SLPopBack(&s);
}
else if(option == 3)
{
printf("请依次输入你要头插的数据个数和数据:>");
int n = 0;
scanf("%d",&n);
for(int i = 0; i < n; ++i)
{
int x = 0;
scanf("%d",&x);
SLPushFront(&s,x);
}
}
else if(option == 4)
{
printf("头删数据:>");
SLPopFront(&s);
}
else if(option == 5)
{
printf("打印数据:>");
Print(&s);
}
else
{
printf("无此选项");
}
}while(option != 0);
SLDestroy(&s);
return 0;
}
总结
优点
访问速度快:
顺序表利用数组实现,可以快速访问任意位置的元素。由于数组元素在内存中是连续存储的,因此可以通过索引直接访问,时间复杂度为 O(1)。
易于实现:
顺序表的实现相对简单,只需要利用数组就可以实现基本的功能。
内存利用效率高(满载时):
当顺序表接近满载状态时,内存利用率较高,因为所有的元素都紧密地存储 在一起,没有额外的开销。
适合静态数据:
对于那些元素数量变化不大或几乎不变的情况,顺序表是一个很好的选择,因为它可以预先分配足够的空间,然后使用而不必担心频繁的扩容或缩容问题。
缺点
插入和删除操作效率低:
在顺序表中插入或删除元素时,可能需要移动大量的元素来保持元素的连续性和正确的顺序。这种移动操作的时间复杂度为 O(n),其中 n 是需要移动的元素数量。因此,在表的中间位置插入或删除元素时,效率会很低。
空间预分配问题:
创建顺序表时需要预先分配足够的空间。如果预估不足,可能导致频繁的扩容操作;如果预估过多,则会造成内存浪费。
扩展性差:
扩展顺序表(即增加容量)通常需要重新分配内存,并将原有数据复制到新的内存空间,这不仅消耗时间,还可能导致额外的内存开销。
不适合频繁变动数据量:
对于那些需要频繁进行插入和删除操作的应用场景,顺序表不是最佳选择,因为这些操作会导致效率下降。