今天我们正式进入了数据结构的相关知识的学习,今天学习的是有关顺序表的相关知识,我们来了解一下。
本文章在从C语言语法基础过渡到数据结构与算法的学习。通过通讯录项目作为实际应用案例,展示为什么需要学习数据结构,以及如何将理论知识应用于实际项目开发。
预备知识
要实现通讯录项目,需要掌握两个关键技术:
- C语言语法基础:
-
结构体、指针、动态内存管理
-
函数封装、模块化编程:
- 数据结构之顺序表/链表
-
线性表的基本概念
-
顺序表的实现与应用
-
链表的实现与应用
1. 顺序表的概念及结构
1.1 线性表
线性表(linear list) 是n个具有相同特性的数据元素的有序序列。线性表是一种在实际中广泛使用的数据结构。
常见的线性表:
-
顺序表
-
链表
-
栈
-
队列
-
字符串
逻辑结构 vs 物理结构:
-
逻辑结构:线性结构,即连续的一条直线
-
物理结构:存储时不一定是连续的,通常以数组或链式结构存储
类比理解:
c
蔬菜分类:绿叶类、瓜类、菌菇类
线性表:具有部分相同特性的一类数据结构的集合
2. 顺序表分类
2.1 顺序表和数组的区别
数组:基础数据结构,固定大小的连续内存空间
顺序表:基于数组的封装,实现了常用的增删改查等接口
2.2 顺序表分类
2.2.1. 静态顺序表
概念:使用定长数组存储元素
c
// 静态顺序表实现
typedef int SLDataType; // 数据类型,方便后续修改
#define N 7 // 固定容量
typedef struct SeqList {
SLDataType a[N]; // 定长数组
int size; // 有效数据个数
} SL;
内存布局示例:
c
索引: 0 1 2 3 4 5 6
数据: [ ] [ ] [ ] [ ] [ ] [ ] [ ]
↑
数组a[N],N=7
静态顺序表的缺陷:
-
空间给少了不够用
-
空间给多了造成浪费
-
无法动态调整大小
2.2.2 动态顺序表(重要)
概念:使用动态分配的数组,可根据需要调整大小
c
// 动态顺序表实现
typedef int SLDataType;
typedef struct SeqList {
SLDataType* a; // 指向动态开辟的数组
int size; // 有效数据个数
int capacity; // 容量大小
} SL;
3. 动态顺序表的实现
3.1基本操作接口设计
c
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* arr;
int size;
int capacity;
}SL;
void SLCheck(SL* ps);
void SLPrint(SL ps);
//顺序表初始化
void SLInit(SL* ps);
//顺序表的销毁
void SLDestroy(SL* ps);
//头部插入删除 / 尾部插入删除
void SLPushBack(SL* ps, SLDataType x);
void SLPushFront(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPopFront(SL* ps);
//void SLBackAdd(SL* ps, int pos, SLDataType x);
//void SLDel(SL* ps, int pos);
静态顺序表适用于数据量已知且固定的场景,而动态顺序表适用于数据量变化较大的场景。
动态顺序表更加灵活,但实现复杂一些,需要处理内存分配和释放。静态顺序表简单,但不够灵活。
在我们的代码中,实现的是动态顺序表,它通过一个结构体来管理数组指针、大小和容量,并提供了扩容机制(当大小等于容量时,容量扩大为原来的2倍)。
代码中各个函数的功能:
c
SLInit:初始化顺序表,将数组指针置为空,大小和容量置为0。
SLDestroy:销毁顺序表,释放动态分配的数组,并将指针置空,大小和容量置0。
SLCheck:检查顺序表容量,若已满则扩容。
SLPushBack:在顺序表尾部插入一个元素,先检查容量,然后将元素放入数组末尾,大小加1。
SLPushFront:在顺序表头部插入一个元素,先检查容量,然后将所有元素后移一位,在头部放入新元素,大小加1。
SLPopBack:删除顺序表尾部元素,只需将大小减1(注意:这里并不释放内存,只是逻辑删除)。
SLPopFront:删除顺序表头部元素,将头部之后的所有元素前移一位,然后大小减1。
SLBackAdd:在指定位置插入元素,先检查位置是否合法,然后检查容量,将该位置及之后的元素后移,插入新元素,大小加1。
SLDel:删除指定位置元素,检查位置合法性,然后将该位置之后的元素前移,大小减1。
注意:在删除操作中,我们并没有实际释放内存,只是通过减小size来标记元素数量减少。真正的内存释放会在销毁顺序表时进行,或者扩容时重新分配。
接下来我们来对这些分装函数一一进行讲解由来:
3.2函数功能实现
首先,由于我们这是一个项目,所以我们应该有属于这个项目的文件,我们这里是关于顺序表的,所以头文件就可以命名为SeqList.h,再然后把函数的声明都放在这个头文件当中,把函数的代码实现放在SeqList.c文件中,再然后我们在test.c文件中去测试就可以了。
如下图:

在开始之前我们再说明一下一下前提流程,为什么我们这里把int 用typedef给变成SLDataType呢?这是因为我们这里如果假设全是int,那么数据类型是不是就定死了呀,但如果我们以后想用这个代码不只处理整型数据,那么我是不是就不用一个一个去修改int,而是直接一键改SLDataType就可以了呢?
因为其他的.c文件只需要包含我们自己创建的这个头文件就行,所以我们在这个头文件中先把我们需要的包含的头文件写出来,这样就会方便也美观许多。
3.2.1顺序表的初始化
c
void SLPrint(SL ps);
//顺序表初始化
这是我们在头文件中需要去声明的,接下来我们到SeqList.c文件中去完成代码的处理:
c
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
在这里我们需要说明一点,我们想要进行初始化等后续操作,这里必须为传址而不能为传值,也就是我们这里需要传指针,这个代码很好理解对吧,对arr赋空指针,接着把空间与个数的值赋为0,到此,顺序表的初始化就完成了。
3.2.2检查扩容函数
c
void SLCheck(SL* ps);
//头文件
c
void SLCheck(SL* ps)
{
if (ps->size == ps->capacity)
{
int Newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, Newcapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc");
exit(1);
}
ps->arr = tmp;
ps->capacity = Newcapacity;
}
}
我们来解释下这个函数的形成,因为我们在进行函数操作时,可能会遇到ps->size == ps->capacity的情况,即空间不足,此时就需要我们去扩容,不过我们这里考虑到有一个问题,那就是我扩容该扩多少才合理呢?
假如我原本空间为8,因为这次空间不足了,我直接下次给它扩容到800000,那我问你,这和静态顺序表有什么区别呢?不是还是会浪费空间嘛,那如果我下次扩容到9呢?是不是太少了呀,每次都要扩容,太过于浪费时间。
所以,当我们发现ps->size == ps->capacity相等时,我去用Newcapacity 接受这个三目表达式的值,如果ps->capacity等于0,那么值为4要不然为2 * ps->capacity,接下来应该没什么好说的了
3.2.3尾插
c
void SLPushBack(SL* ps, SLDataType x);
c
void SLPushBack(SL* ps, SLDataType x)//尾插
{
assert(ps);
SLCheck(ps);
ps->arr[ps->size++] = x;
}
这里为了防止传过来的为空指针,去用assert去断言一下,如果为空指针,那么就会直接报错,这里我们就是调用了上面的函数,扩容成功后就在最后位置插入我们想要的值即可,插入后不要忘了ps->size++。
3.2.4头插
c
void SLPushFront(SL* ps, SLDataType x);
c
void SLPushFront(SL* ps, SLDataType x)//头插
{
assert(ps);
SLCheck(ps);
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];//arr[1]=arr[0];
}
ps->arr[0] = x;
ps->size++;
}
如下图:

我们只需要从最后一个元素开始不断的向后移动,直到把第一个位置空出来即可~
所以这里写一个for循环当空出来后,对第一个位置进行插入,然后ps->size++即可
3.2.5尾删
c
void SLPopBack(SL* ps);
c
void SLPopBack(SL* ps)//尾删
{
assert(ps->size);
assert(ps->arr);
--ps->size;
}
我们这里还是检查问题,因为要进行尾删,实际上只需要--ps->size即可,因为我们打印时就是看ps->size
3.2.6头删
c
void SLPopFront(SL* ps);
c
void SLPopFront(SL* ps)//头删
{
assert(ps->size);
assert(ps->arr);
for (int i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}

我们想要进行头删,实际上就是从第二个开始把每个元素向前移动是不是就可以了?
移动完之后ps->size--即可
3.2.7在指定位置之前插入元素
c
void SLBackAdd(SL* ps, int pos, SLDataType x);
c
void SLBackAdd(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheck(ps);
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;
}
这个其实也很好理解,既然是在在指定位置之前插入元素,那么我就让那个指定位置之后的元素都往后移动就可以了,然后插入元素,最后ps->size++即可
3.2.8删除指定位置的数据
c
void SLDel(SL* ps, int pos);
c
void SLDel(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheck(ps);
for (int i = pos; i < ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
这个代码的逻辑为,既然是删除指定位置的数据,那么只需要从要删除的这个数据开始,让后一个去覆盖前一个就好了,这样就达到了我们想要的效果,当删除后别忘了ps->size--
3.2.9打印
最简单的一集来了奥!
c
void SLPrint(SL ps);
c
void SLPrint(SL ps)
{
for (int i = 0; i < ps.size; i++)
{
printf("%d ", ps.arr[i]);
}
}
没事好说的,过!
3.2.10顺序表的销毁
c
//顺序表的销毁
void SLDestroy(SL* ps);
c
void SLDestroy(SL* ps)
{
if (ps->arr != NULL)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
当传过来的不是空指针时那么就free掉,再然后置为空指针,都赋为0即可。
3.3测试
我们只需要在test.c文件中去测试我们代码的逻辑有没有问题即可,我们来举个例子,测试下尾插和指定位置删除:


本篇文章到此结束,敬请期待下一篇的到来