⭐顺序表
💎定义
数据的存储按特定的逻辑或物理顺序连成一个序列的存储方式叫**线性存储结构**,是由相同类型构成的有序序列的线性结构。
特点:
-
线性表中的元素个数长度就为线性表的长度。
-
线性表没有元素时,为空表。
-
线性表的起始位置为表头,结束位置为表尾。
💎基本结构
typedef int SeqLElemType;
typedef struct SeqList
{
SeqLElemType* arr;
int size;
int capacity;
}SL;
说明:
✨元素类型定义和顺序表结构体定义
-
本例中顺序表元素都以整型int作为参考模板,因此将整型类型重命名为SeqLElemType,表示线性表内部元素的类型,以该种类型重命名的好处是,后续如果更改顺序表数据类型不用逐一替换每个文件中使用到的变量类型,而可以统一在头文件更改即可。
-
定义一个顺序表结构体,作为该线性结构的基本形式。其中包含了三大部分:
🥇数据类型指针
🥈顺序表有效元素个数标识
🥉顺序表最大容量。
-
在该结构体中使用指针而不定义数组的最大原因在于,一个数组的定义必须给定其固定的大小作为预处理时该数组所拥有的固定空间,但是因为是静态数组,所以是不可扩容的,当想存入更多的数据时,会因为其最大容量受到限制:
#define 50 MAX int arr[MAX] = {0}; //固定的大小空间,所存储元素个数不能超过50个 int arr* = NULL; //一个整型指针,其所指向的空间可以通过扩容函数动态开辟
-
剩下两个就很好理解了,两个变量size和capacity,前者意义是顺序表中存了多少个有效数据,size就为多少;后者为当前顺序表的最大容量(比如为4个SeqLElemType大小),当想存入更多元素时,该capacity可被扩容到更大数值,即该顺序表就能存放更多元素了。
-
这里需要注意的是,size因为标识了当前顺序表的有效数据个数,如当顺序表为空时,size为0;当有1个元素时,size为1。利用这个特点可以将size视作在顺序表中存储的有效数据的后一个下标位,当有数据需要存入(比如尾插)时,就可以通过该size所指示的下标直接存入,再将size++,达到一个比较良好的循环。
💎顺序表基本功能
✨顺序表结构基本增删查改功能概览
//顺序表相关接口
SL* SLInit(); //顺序表初始化
void SLInit2(SL* seq); //两种初始化顺序表方式
void SLCapacityCheck(SL* seq); //扩容检查
bool SLEmpty(SL* seq); //判空
void SLPushBack(SL* seq, SeqLElemType x); //尾插
void SLPopBack(SL* seq); //尾删
void SLPushFront(SL* seq, SeqLElemType x); //头插
void SLPopFront(SL* seq); //头删
void SLInsert(SL* seq, int pos, SeqLElemType x); //中间插入
void SLErase(SL* seq, int pos); //中间删除
void SLModify(SL* seq, int pos, SeqLElemType x); //修改下标对应元素
void SLDestroy(SL* seq); //顺序表销毁
int SLFindPos(SL* seq, SeqLElemType x); //查找元素对应下标
SeqLElemType SLFind(SL* seq, int pos); //查找下标对应元素
//通用函数
void SLPrint(SL* seq); //打印顺序表
✨顺序表初始化
-
与任何数据变量一样,顺序表在头文件中仅定义了其结构体类型和数据类型,此时并不能直接定义给结构体变量使用,需要首先完成结构体及其内部数据的初始化:
SL* SLInit() //顺序表初始化 { SL* New = (SL*)malloc(sizeof(SL)); New->arr = NULL; New->capacity = New->size = 0; return New; } SL* sq = SLInit();
-
上述方式为一种初始化结构体指针变量的方式,为什么定义和初始化一个结构体我们选择创建了一个结构体指针?因为后续我们对这个顺序表进行增删查改操作的时候,都是通过取这个顺序表的地址对其内部的数据进行解引用访问,而不能直接通过简单的结构体传值访问,举一个简单的例子:
struct student //定义一个学生信息结构体,仅包含年龄 { int age; }stu, *stu1; //定义一个结构体变量,一个结构体指针变量 void Change(struct student s) //通过传结构体形参变量改变实参 { s.age = 30; } void Change1(struct student* s) //通过传结构体形参指针变量改变实参 { s->age = 30; } int main() { stu = { 20 }; //结构体初始化 stu1 = &stu; printf("%d\n", stu.age); printf("%d\n", stu1->age); Change(stu); printf("%d\n", stu.age); Change1(stu1); printf("%d\n", stu1->age); return 0; }
两种不同的传结构体方式,可得到如下结果
20 20 20 30
🎈由此可知,如果我们想操作一个结构体内部的数据,必须通过传入这个结构体的地址对其进行修改,类似的规律可以总结如下:
//要改变int,要传int的地址,解引用改变 int x = 0; f1(&x); printf("%d\n",x); //要改变int*,要传int*的地址,解引用改变 int* ptr = NULL; f2(&ptr); printf("%p\n", ptr);
-
依据同样的道理,观察顺序表初始化函数,我们并没有传入一个结构体指针进去来初始化该结构体内部的数值,而是选择不传入数据,这样是因为注意观察函数内部,其返回了一个参数New,并且该参数类型为SL*,也就是说虽然没有传参进入,但在初始化函数内部新建了一个局部结构体指针,并完成了🥇结构体空间的开辟,🥈数组指针的置空,以及🏅容量和大小标识的置0。将这些局部有效的信息通过返回值返回,便最终通过地址改变了实参sq。
-
除了不传参通过返回值的形式初始化内存空间,还有一种方式能达到相同的效果:
🍕通过对顺序表结构体变量取地址
void SLInit2(SL* seq) { assert(seq); seq->arr = NULL; seq->capacity = seq->size = 0; } SL sq; SLInit2(&sq);
本质与上个方法相同,不同在于其是带参函数,参数类型为结构体指针,将实参的结构体地址传入并在内部通过解引用对结构体内部数据修改,不带返回值也可行。
✨顺序表扩容
一个顺序表的初始化只是开辟了指向该顺序表相关信息(如数组地址,容量和大小)的空间和将结构体内部的数据置空以防止成为随机值或野指针,但这并不代表我们可以真正使用这个顺序表去存放相关数据和信息,还需要对其扩容。
void SLCapacityCheck(SL* seq) //扩容检查
{
assert(seq);
if (seq->capacity == seq->size)
{
int expand = seq->capacity == 0 ? 4 : seq->capacity * 2;
SeqLElemType* NewCapacity = (SeqLElemType*)realloc(seq->arr, sizeof(SeqLElemType) * expand);
assert(NewCapacity);
seq->capacity = expand;
seq->arr = NewCapacity;
}
}
说明:
-
将我们已经初始化完成的结构体指针作为参数传入到该函数中,就可以对未开辟任何空间的顺序表进行扩容,扩容目的无他,就是为了准备数据的存入。
-
先加入一条断言声明assert,保证传入的这个结构体地址不是空指针或野指针,如果是则直接报错,是一种预防式的编程规范。
-
一个顺序表(也可以说一个数组)在什么情况下无法存入数据?可归类为两种情况:
⭐顺序表为空
⭐顺序表为满
两种情况下都无法继续存放数据,因为顺序表为空时,容量capacity为0,有效数据个数size也为0;而当顺序表为满时,比如一个数组能存放4个数据,此时容量capacity为4,存放的4个有效数据size也为4,可以判断顺序表无法继续存后续数据,所以判断条件写为capacity == size,但是需要注意这两个变量的取值是从结构体内部拿出来的,所以需要通过结构体指针sq->来解引用访问。
-
观察代码,可注意到我们通过创建两个中间的临时变量expand和NewCapacity来为真正扩容实参数据前进行临时存放,这样做的目的是为了更安全和稳定的将扩容的数据个数expand和已扩容的内存地址NewCapacity存入结构体真正的容量seq->capacity和数组首元素地址seq->arr中,以防止动态开辟内存realloc失败时返回的-1赋给实参的情况。
-
这里还需要注意🥵,为数组动态扩容所用到的库函数是realloc而非malloc,因为如果一开始开辟的内存空间已经存不下后续值,需要重新扩容时使用的是调整内存空间大小函数realloc,而该函数在一开始未开辟任何空间时也能充当malloc来初始开辟内存的功能,为一举两得的用法。
✨顺序表判空
当一个顺序表的有效数值个数size为0时,则可判定其内部为空,此时顺序表的容量可能为0(仅进行了初始化),也可能不为0(进行了扩容,但还没存放数据或将原有数据全部删除)。
bool SLEmpty(SL* seq) //判空
{
assert(seq);
return seq->size == 0;
}
说明:
- 返回值引用了布尔类型,当顺序表为空时,判断seq->size为0,而0 == 0为真,则返回true(true代表数值1,即非0值)。
- 当顺序表不为空时,此时seq->size不为0,此时size != 0,返回false(即数值0)。
✨顺序表遍历
顺序表遍历通常可以认为就是对数组的遍历,但顺序表与数组最大性质的区别就在于🥵虽然两者都可以通过随机的下标访问取到各个位置的数据,但是顺序表的存储必须是连续的,即不能前面为空,在后面存放数据,或者前后存入数据,中间不存入数据。
void SLPrint(SL* seq)
{
assert(seq);
int traverse = 0;
printf("当前顺序表结构:");
while (traverse < seq->size)
{
printf("%d ", seq->arr[traverse]);
traverse++;
}
printf("\n");
}
序表的存储必须是连续的,即不能前面为空,在后面存放数据,或者前后存入数据,中间不存入数据。
void SLPrint(SL* seq)
{
assert(seq);
int traverse = 0;
printf("当前顺序表结构:");
while (traverse < seq->size)
{
printf("%d ", seq->arr[traverse]);
traverse++;
}
printf("\n");
}
To be continue…