📒博客主页:要早起的杨同学的博客
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文所属专栏:【数据结构】
✉️坚持和努力从早起开始!
💬参考在线编程网站:🌐牛客网🌐力扣
🙏作者水平有限,如果发现错误,敬请指正!感谢感谢!

#define N 200
typedef int SeqDataType;
typedef struct SeqList
{
SLDataType a[N]; //定长数组
int size; //有效数据个数
}SeqList;

typedef int SeqDataType; //类型重命名,后续要存储其它类型时方便更改
typedef struct SeqList
{
SeqDataType* a; //指向动态开辟的数组
size_t size; //有效数据个数
size_t capacity; //容量大小
}SeqList;
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。
所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
什么是接口函数?
接口函数是在数据进行操作时进行调用的函数,通过调用数据结构的接口帮助你完成一系列操作.
首先新建一个工程( 博主使用的是 VS2022 )此次用的是动态顺序表
如图:

#pragma once //防止头文件被二次引用
#include /*perror, printf*/
#include /*assert*/
#include /*realloc*/
typedef int SeqDataType; //后续要存储其它类型时方便更改
//顺序表的动态存储
typedef struct SeqList
{
SeqDataType* a; //指向动态开辟的数组
size_t size; //有效数据个数
size_t capacity; //容量大小
}SeqList;
//初始化顺序表
void SLInit(SeqList* s);
//销毁顺序表
void SLDestory(SeqList* s);
//检查顺序表容量是否满了,好进行增容
void CheckCapacity(SeqList* s);
//顺序表尾插
void SLPushBack(SeqList* s, SeqDataType x);//O(1)
//顺序表头插,挪动数据
void SLPushFront(SeqList* s, SeqDataType x);//O(N)
//顺序表尾删
void SLPopBack(SeqList* s, SeqDataType x);//O(1)
//顺序表头删,挪动数据
void SLPopFront(SeqList* s, SeqDataType x);//O(n)
//打印顺序表
void SLPrint(const SeqList* s);
//查找顺序表里面的元素,找到返回下标,找不到返回-1
int SLFind(SeqList* s, SeqDataType x);
//在顺序表中间pos位置插入元素
void SLInsert(SeqList* s, size_t pos, SeqDataType x);
// 顺序表删除pos位置的值
void SLErase(SeqList* s, size_t pos);
//查看顺序表中数据个数
size_t SeqListSize(const SeqList* s);
//修改指定下标位置的数据
void SeqListAt(SeqList* s, size_t pos, SeqDataType x);
这里重点讲解 SeqList.c 中各个接口函数的实现。
首先引入我们自己创建的头文件 #include “SeqList.h” ,我们就可以开始动手实现顺序表初始化函数了。
首先通过 s 指向 a,将数组为空。因为是初始化,所以将有效数据个数和数组时即能存数据的空间容量一并置为0。
记得一定要加上断言,防止传进来的指针为空
void SLInit(SeqList* s)
{
assert(s); //断言
s->a = NULL; //初始顺序表为空
s->size = 0; //初始数据个数为0
s->capacity = 0; //初始空间容量为0
}
记得一定要加上断言,防止传进来的指针为空
void SLDestory(SeqList* s)
{
assert(s); //断言
free(s->a); //释放动态开辟的空间
s->a = NULL; //置空
s->size = 0; //数据个数置0
s->capacity = 0; //空间容量大小置0
}
后续我们会插入元素,如果空间不够,则使用realloc函数扩容。
newcapacitv是扩容后的内存空间,newA是指向这个新的空间的指针。如果空间为0,则扩容可以放置4个元素的空间,如果空间已满,则在原来的空间基础上,增加1倍,为什么不采取插一个数据,增容一个空间的方式呢,因为这样也太麻烦了,代价也太大了,一般情况下,为了避免频繁的增容,当空间满了后,我们不会一个一个的去增,而是一次增容 2 倍,当然也不会一次增容太大,比如 3 倍 4 倍,空间可能会浪费,2 倍是一个折中的选择。
void CheckCapacity(SeqList* s)
{
assert(s); //断言
if (s->size == s->capacity) //检查容量,满了则增容
{
size_t newcapacity; //新容量
if (s->capacity == 0)
newcapacity = 4; //原来容量为0,扩容为4
else
newcapacity = 2 * s->capacity; //原来容量不为0,扩容为原来的2倍
SeqDataType* newA = (SeqDataType*)realloc(s->a, sizeof(SeqDataType) * newcapacity); //扩容
if (newA == NULL)
{
perror("CheckCapacity");
exit(-1);
}
s->a = newA; // newA不为空,开辟成功
s->capacity = newcapacity; //更新容量
}
}
尾插有三种情况:
① 第一种情况是顺序表压根就没有空间。
② 第二种情况就是我们创建的 capacity 空间满了。
③ 第三种情况是空间足够,直接插入数据即可。
void SLPushBack(SeqList* s, SeqDataType x)
{
assert(s); //断言
CheckCapacity(s); //检查顺序表容量是否已满
s->a[s->size] = x; //尾插数据
s->size++; //有效数据个数+1
}

不知道 SeqDataType是什么类型的数据,不能冒然的将顺序表最后一个数据赋值为 0,我们只需将有效数据个数 size 减 1 即可达到尾删的目的。
void SLPopBack(SeqList* s, SeqDataType x)
{
assert(s); //断言
assert(s->size > 0); //顺序表不能为空
//不知道SeqDataType是什么类型的数据,不能冒然的赋值为0
//s->a[s->size - 1] = 0;
s->size--; //有效数据个数-1
}

思路:首先创建一个 end 变量用来指向要移动的数据,因为指向的是数据的下标,所以是 size 要减 1 。随后进入 while 循环,如果 end >= 0 说明还没有移动完,就会进入循环。循环体内利用下标,进行向后移动操作,移动结束后再 end-- ,进行下一个数据的向后移动。挪动数据成功后,就可以插入了。此时顺序表第一个位置就被腾出来了,就可以在下标0位置插入欲插入的数据 x 了。最后记得 size++ 。
void SLPushFront(SeqList* s, SeqDataType x)
{
assert(s); //断言
CheckCapacity(s); //检查顺序表容量是否已满
int end = s->size - 1;
while (end >= 0)/顺序表中[0,size-1]的元素依次向后挪动一位
{
s->a[end + 1] = s->a[end];
end--;
}
s->a[0] = x;//头插数据
s->size++;//有效数据个数+1
}
思路和头插类似,依次向前挪动,然后数量-1即size–
如果头删多次调用,如何内存中已经没有元素了,size依然在减。所以这里依然会出现越界,所以需要断言。
void SLPopFront(SeqList* s)
{
assert(s);//断言
assert(s->size > 0);//顺序表不能为空
int begin = 1;
while (begin < s->size)//顺序表中[1,size-1]的元素依次向前挪动一位
{
s->a[begin-1] = s->a[begin];
begin++;
}
s->size--;//有效数据个数-1
}

void SLPrint(const SeqList* s)
{
assert(s);
if (s->size == 0) //判断顺序表是否为空
{
printf("顺序表为空\n");
return;
}
for (int i = 0; i < s -> size; i++)//打印顺序表
{
printf("%d ", s->a[i]);
}
printf("\n");
}
//查找顺序表里面的元素,找到返回下标,找不到返回-1
int SLFind(SeqList* s, SeqDataType x)
{
assert(s);
for (int i = 0; i <s->size; i++)
{
if (s->a[i] == x)
{
return i;
}
}
return -1;
}

顺序表要求数据从第一个开始放并且数据是连续存储的,所以我们就要注意下指定的下标位置 pos 的位置了!有些位置并不可以,所以我们需要进行一些检查。
将指定位置后的所有数据依次向后挪动一位,初始代码如下:
int i = 0;
for (i = s->size - 1; i >= pos; i--)
s->a[i + 1] = s->a[i];
下面这样写就避免 i 变成 -1 负数了
//在顺序表中间某个位置插入元素
void SLInsert(SeqList* s, size_t pos, SeqDataType x)
{
assert(s);
assert(pos >= 0 && pos < s->size);//检查pos下标的合法性
CheckCapacity(s);//检查顺序表容量是否已满
size_t end = s->size;
while (end > pos)//将pos位置后面的数据依次向后挪动一位
{
s->a[end] = s->a[end-1];
--end;
}
s->a[pos] = x;//插入数据
s->size++;//有效数据个数+1
}

void SLPushFront(SeqList* s, SeqDataType x)
{
SLInsert(s, 0, x); //改造头插接口
}
删除指定位置的数据,我们仍然要限制 pos 的位置。限制条件部分和 SLInsert不同的是,因为 s->size 这个位置没有效数据,所以删除的位置不能是 s->size!
void SLErase(SeqList* s, size_t pos)
{
assert(s);//断言
assert(pos >= 0 && pos < s->size);//检查pos下标的合法性
size_t end = pos;
while (end < s->size-1)//将pos位置后面的数据依次向前挪动一位
{
s->a[end] = s->a[end+1];
end++;
}
s->size--;//有效数据个数-1
}

//顺序表头删
void SLPopFront(SeqList* s)
{
SLErase(s, 0); //改造头删接口
}
size_t SeqListSize(const SeqList* s)
{
assert(s); //断言
return s->size;
}
越界不一定报错,系统对越界的检查是一种抽查
- 越界读一般是检查不出来的
- 越界写如果是修改到标志位才会检查出来
(系统在数组末尾后设的有标志位,越界写时,恰好修改到标志位了,就会被检查出来)
void SeqListAt(SeqList* s, size_t pos, SeqDataType x)
{
assert(s); //断言
assert(s->size > 0); //顺序表不能为空
assert(s >= 0 && pos < s->size); //检查pos下标的合法性
s->a[pos] = x; //修改pos下标处对应的数据
}
t SeqListSize(const SeqList* s)
{
assert(s); //断言
return s->size;
}