顺序表 结构体的定义:
静态数据结构的结构体
typedef int SLDataType;
#define N 100
//静态的顺序表结构体
struct SeqList
{
SLDataType a[N];
int size; //数据个数
};
动态数据结构的结构体
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;
int size; //有效数据个数 (当有效数据个数更空间大小一样的时候就可以进行扩容)
int capacity; //空间容量
}ST;
动态开辟和柔性数组的区别
动态开辟是用一个结构体里的指针指向一块空间,然后这个结构体里面有关于这块空间的一些信息,在动态开辟中新开辟空间,用的是realloc函数来在后面这块空间中扩大,但是柔性数组就是一块空间,只是在这块空间的开始位置存入了size capacity这些数据。
顺序表的管理 -- 增删查改
顺序表的初始化函数:
//初始化函数
#define INIT_CAPACITY 10 //定义顺序表初始化的空间大小
void SLInit(ST* ps)
{
ps->a = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY); //开辟一块空间
// 判断malloc函数是否开辟空间成功
if (ps->a == NULL)
{
perror("malloc fail");
}
ps->size = 0;
ps->capacity = INIT_CAPACITY;
}
void textSLlist() //初始化函数的运用
{
SL s;
SLInit(&s);
}
顺序表的删除函数
//删除顺序表的函数
void Destroy(ST* ps)
{
free(ps->a);
ps->capacity = 0;
ps->size = 0;
}
顺序表的尾插函数
//尾插顺序表数据函数
void SLPushBack(SL* ps , int x)
{
//判断这个顺序表是否是满的
if (ps->size == ps->capacity)
{
// 顺序表来说一般一次扩容2倍是最合理的
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
//判断realloc是否成功
if (tmp == NULL)
{
perror("realloc fail");
}
ps->capacity *= 2;
}
ps->a[ps->size] = x; // size指向的位置就是顺序表中最后一位有效数据的后一位
ps->size++;
}
free 指针空间出错主要是两种可能:
首先是野指针,野指针中也分为两种情况,一是这个指针是空的,而是我们free这个的这个指针指向的空间不是开头,是中间位置,如下图所示:
我们在free空间的时候必须从我们开辟空间的起始位置开始释放,从中间其他位置释放都是会报错的。
二是这个指针访问越界,这个和数组访问越界是一样的,都会报错
顺序表的扩容函数
//扩容函数
void SLCheckCapacity(SL* ps)
{
assert(ps);
//判断这个顺序表是否是满的
if (ps->size == ps->capacity)
{
// 顺序表来说一般一次扩容2倍是最合理的
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
//判断realloc是否成功
if (tmp == NULL)
{
perror("realloc fail");
}
}
}
顺序表有了扩容为什么没有缩容呢?因为计算机的内存不支持这样操作,我们在扩容是在原本的内存空间的基础之上进行扩容,扩容之后视为一块空间,但是free空间只能释放一块空间。
顺序表的尾删函数
//尾插函数
void SLPushBack(SL* ps,SLDataType x)
{
assert(ps); //断言
SLCheckCapacity(ps); // 扩容函数(函数内有判断该顺序表有没有满)
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
}
头插的时间复杂度:O(n^2)
尾插的时间复杂度:O(n)
顺序表的头插函数
//头插函数
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0); //当顺序表中没有数据的时候size不能在size--
int begin = 1; //begin指向顺序表的第二个数据
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
头删和头插是一样的,n个数据的使用都是时间复杂度都是等差数列的叠加,都是O(n^2)。
指定位置插入数据
//指定位置插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert((pos >= 0) && (pos <= ps->size)); //判断pos的输入是否符合条件
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->[pos] = x;
ps->size++;
}
指定位置删除数据
//指定位置删除数据
void SLErase(SL* ps, int pos)
{
assert(ps);
assert((pos >= 0) && (pos < ps->size)); //判断pos的输入是否符合条件
//此处间接检查了顺序表为空的情况
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
查找数据
//查找数据
void SLFind(SL* ps, SLDataType x)
{
assert(ps);
int prev = 0;
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
return i; //找到了就返回这个数据对应的下标
}
}
return -1; //没有找到就返回-1
}
realloc的原地扩容和异地扩容
假设我们要在之前的空间基础之上扩容5个大小的空间,如果在原本空间的后面有足够大小的空间,那么就直接在原本空间后面直接原地扩容,原地扩容的相比于异地扩容的效率是很高的,如下图所示:
而异地扩容是在内存的其他位置开辟一块更大的空间,这块空间可以容纳下原始空间和新开辟空间的所有数据。然后把原始空间里的数据复制到新的空间中,再把原来的空间释放掉,
当我们扩容之后不需要再使用这么大的空间了,那我们可以再次使用realloc来进行缩容(回收),如下所示:
//realloc 缩容
int main()
{
int* ptr1 = (int*)malloc(sizeof(int) * 10); //malloc一块新的空间
printf("%s\n", ptr1); //打印ptr这块空间的其实地址
int* ptr2 = (int*)realloc(ptr1,sizeof(int) * 20); //把这块空间realloc扩大到二十的大小,
printf("%s\n", ptr2); // 打印此时地址位置,如果与上面的地址不同,则是异地扩容,相同则是原地扩容
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 5); //realloc缩容操作,把这块空间缩容到5的大小
printf("%s\n", ptr3);
for (int i = 0; i <= 20; i++)
{
ptr3[i] = i; //我们在缩容之后再去按照扩容的大小去访问这块空间,那么此时就会报错,数组访问越界
}
free(ptr3);
return 0;
}
虽然realloc可以实现缩容,但是我们不建议这样做,因为假设我们把原本空间给缩容了,在之后的内存申请中,把这块空间后面的空间给申请了,那么我们之后再对这块空间进行扩容的时候就可能会使用realloc的异地扩容,而异地扩容相对于原地扩容来说效率低了不少,而且我们realloc大多数申请的空间占得空间也不会很多。
那么我们在顺序表中,就只扩不缩。
总结
上述就是利用c语言实现顺序表基本操作函数,我们也可以对其进行优化,比如写一个菜单来使用这些操作函数,来更好的帮助我们来使用这些操作函数。