顺序表(C语言实现)

发布于:2024-04-10 ⋅ 阅读:(37) ⋅ 点赞:(0)

 

什么是顺序表

顺序表和数组的区别 

顺序表本质就是数组 

结构体初阶+进阶 系统化的学习-CSDN博客

简单解释一下,就像大家去吃饭,然后左边是苍蝇馆子,右边是修饰过的苍蝇馆子,但是那个好看的苍蝇馆子一看,这不行啊,我这个和你的东西一样名字一样,我这个吸引不来客户,那我改个名字,叫米其林。

就像顺序表和数组之间的区别。

顺序表属于什么

顺序表既是一个数据结构,也是C语言中可以使用的一种数据组织方式。
作为数据结构,顺序表是一种抽象的数据类型,它指定了一种线性数据集合,其中的元素按照一定的顺序排列,并且可以通过索引来访问。顺序表在许多编程语言中都有实现,包括C语言。
在C语言中,顺序表通常通过数组来实现。数组是C语言提供的一种基本数据类型,它可以用来存储一系列相同类型的数据。数组的长度在定义时确定,但C语言也支持动态分配内存的方式来创建可变大小的数组,这相当于实现了动态顺序表。
因此,顺序表作为一个概念,属于数据结构领域;而在C语言中,顺序表可以通过数组或动态分配的数组来实现。 

顺序表的特点

 顺序表是一种线性表,其底层结构是数组。顺序表有以下几个重要的点:
1. 顺序表有两种类型,静态顺序表和动态顺序表。静态顺序表的大小固定,通常使用定长数组实现,适用于数据量已知的情况。动态顺序表可以根据需要动态调整大小,适用于数据量未知或者变化频繁的情况。
2. 顺序表支持随机访问,即可以通过索引直接访问数组中的任意一个元素,时间复杂度为O(1)。
3. 顺序表的插入和删除操作通常在表尾进行,这样不需要移动多余的元素,时间复杂度为O(1)。当表满时,需要先进行扩容操作,扩容的时间复杂度可能为O(n)。
4. 顺序表的所有操作,包括插入、删除、查找、修改等,都需要借助数组实现。在使用顺序表时,需要注意数组的越界问题。
5. 顺序表在存储空间连续的情况下,能够实现较快的数据传输和处理,但在存储空间不连续的情况下,会带来访问速度上的损失。

静态顺序表

静态顺序表就是我给定一个空间,这里可以存放你需要的内容

举个栗子:你想要创建一个图书管理系统,那这个学校一共俩人三人的,那你给定空间就好了,没有必要费劲巴力的写那麽多代码,虽然可能会造成空间的浪费,但是还是比较少的。

但是需要知道的是,一旦超过你需要的空间,就会导致数据的丢失。

静态顺序表适合以下场景:

  1. 数据量已知:当应用程序在设计时已经知道将要存储的数据量,并且这个数据量在运行时不会发生变化时,使用静态顺序表是一个很好的选择。

  2. 数据不经常改变:如果数据集合在大部分时间内保持稳定,只有偶尔的插入或删除操作,静态顺序表可以提供高效的存储和访问。

  3. 内存空间有限:在内存资源有限的环境中,使用静态顺序表可以避免动态内存分配带来的开销,因为静态顺序表在编译时就已经分配了固定大小的内存。

  4. 性能要求高:静态顺序表的访问速度通常比动态顺序表快,因为不需要进行动态内存分配或扩容操作。在性能要求极高的应用中,静态顺序表可能是一个更好的选择。

  5. 简单应用:对于简单的应用程序或教学环境,静态顺序表由于其简单性和易于理解,是一个合适的数据结构选择。

  6. 固定大小的数据集:例如,存储固定数量的已知数据项,如日期、代码表或其他常量集合,这些数据集在运行时不需要扩展。

动态顺序表

动态顺序表那就是这个空间是可以变化的

就像抖音创建账号一样,如果你就给100万个账号的空间,那么静态顺序表,很快就使用完毕了。抖音当时使用的时候也想不到一晚上增容一个亿。所以这个时候我适合使用动态顺序表。

动态顺序表适合以下场景:

  1. 数据量变化:当应用程序需要处理的数据量在运行时可能会发生变化,或者数据量事先未知时,动态顺序表是一个很好的选择。它可以根据需要动态地调整大小,以适应不断变化的数据量。

  2. 数据量未知:在处理大量数据或不确定数据量的情况下,动态顺序表可以开始时分配一个较小的容量,随着数据的增加逐渐扩容,避免一开始就分配过多不必要的内存。

  3. 频繁插入和删除:动态顺序表适合频繁进行插入和删除操作的应用场景。由于它可以在末尾方便地进行这些操作,不需要像数组那样在每次操作时移动大量元素。

  4. 性能要求不是首要考虑:虽然动态顺序表的插入和删除操作在大多数情况下速度很快,但在扩容时可能会涉及到较慢的内存重新分配。如果性能要求不是主要考虑因素,或者可以通过其他方式优化性能,那么动态顺序表是一个合适的选择。

  5. 需要灵活性:在需要根据应用程序的需求灵活调整数据结构大小的场景中,动态顺序表提供了这种灵活性。它可以适应不同规模的数据集,而无需预先定义数据集的大小。

  6. 内存使用不是关键限制:动态顺序表可能会比静态顺序表消耗更多的内存,因为它需要额外的空间来存储指针。如果应用程序的内存使用不是关键限制因素,或者可以通过其他方式优化内存使用,那么动态顺序表是一个合适的选择。

顺序表和线性表

 顺序表是线性表的一种实现方式。线性表是一个具有相同特性的数据元素的有限序列,其中每个元素都仅有一个直接前驱和直接后继。顺序表通过一组地址连续的存储单元依次存储线性表中的各个元素,使得线性表中在逻辑结构上相邻的元素存储在相邻的物理存储单元中。顺序表有两种类型,分别是静态顺序表和动态顺序表,它们之间的主要区别在于内存分配方式不同。静态顺序表使用定长数组存储元素,而动态顺序表则使用动态开辟的数组存储,其大小可以根据需要动态调整。

简单的说就是具有相同特性的数据结构的集合。 

顺序表可以实现手机的通讯录的项目

讲解完顺序表我们会讲解通讯录项目 

在手机通讯录中,通常需要存储联系人的姓名、电话号码、电子邮件等信息,这些信息可以被组织成一个顺序表。每个联系人信息可以作为一个元素存储在顺序表中,而顺序表的每个元素可以包含多个字段,例如姓名、电话号码等。 

———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 

顺序表的实现逻辑

顺序表实现涉及的点

1. 数据存储结构:

顺序表通常使用数组来实现,因为数组提供了随机访问元素的能力,且在数组的基础上可以很容易地实现插入和删除操作。数组的大小在静态顺序表中是固定的,而在动态顺序表中可以根据需要动态调整。
2. 索引机制:

顺序表通过索引来访问元素,索引通常是一个整数,表示元素在数组中的位置。由于数组索引是从0开始的,因此数组中的第一个元素可以通过索引0访问,最后一个元素可以通过索引n-1访问,其中n是数组的长度。
3. 扩容和缩容:

动态顺序表需要在数组满时进行扩容,即增加数组的容量以容纳更多元素。扩容通常涉及到创建一个新的更大的数组,并将现有元素复制到新数组中。当数组中实际存储的元素数量远小于容量时,可以进行缩容以节省内存。
4. 插入操作:

向顺序表插入元素时,通常在数组的末尾进行操作。如果数组未满,直接在末尾添加新元素;如果数组已满,则需要先扩容,然后再插入元素。
5. 删除操作:

从顺序表删除元素时,通常也是在数组的末尾进行操作。删除操作后,需要将后续的元素向前移动一个位置以填补空缺。如果删除的是最后一个元素,则不需要移动。如果数组变得较小,可以考虑进行缩容。
6. 访问操作:

顺序表支持通过索引直接访问元素,这使得访问时间复杂度为O(1)。也可以通过索引来修改表中的元素。
7. 迭代:

顺序表可以很容易地进行迭代,即按照元素的顺序逐个访问每个元素。迭代通常使用循环结构实现。
8. 内存管理:

动态顺序表需要管理内存分配和释放,以避免内存泄漏和溢出。在C语言等需要手动管理内存的编程语言中,这通常涉及到使用`malloc`、`realloc`和`free`等函数。
9. 数据类型:

顺序表可以存储任何类型的数据,包括数字、字符串、对象等。
10. 容量和大小:

顺序表在创建时分配一个初始容量,用于存储元素。容量是指数组可以容纳的最大元素数量。大小是指当前存储在顺序表中的元素数量。

顺序表实现的白话解释

1.首先我们需要创建三个文件也就是头文件,实现文件,以及测试文件

2.我们需要在顺序表的头文件里面创建一个结构体,进行声明,并且创建增删减改的函数。

3.但是在此之前我们使用的逻辑是动态开辟的空间的顺序表的逻辑,所以我们需要的是开辟空间,那么开辟空间还需要销毁空间,使用开辟的空间之前我们需要初始化空间。

4.初始化空间之后,我们可以对顺序表的内容进行增删减改,那么增删减改里面又涉及到(头部删除)(尾部删除)(头部插入)(尾部插入)(指定位置插入)(指定位置删除)

5.最后我们可以打印出来,或者监视,观察是否插入成功。

6.所以我们产生的逻辑可以是

所以我们顺序表的基本逻辑已经完成

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

顺序表的实现

这里我们是按照上述的实现的白话解释为逻辑进行实现的。

下面让我来进行讲解

1.创建三个文件(顺序表)

2.在头文件里面创建结构体(顺序表)

结构体初阶 知识点的补充(初始化,typedef,访问,声明,传参,类型,调用)-CSDN博客

这里为了方便下面的书写,我们把顺序表名字重命名为SL  

这里可以看见,我们定义类型的时候,不是直接用int,而是定义了一个类型,就像宏定义一样,这里的目的是可替代性强,

拿这个举例:通讯录的实现逻辑是顺序表,但是结构类型不是int类型,而是结构体的类型,那么此时如果我们一个一个进行修改的话,很麻烦。但是要是直接定义类型的话,我们只需要更改一行代码,就可以完成。

typedef int SLDataType;//这里定义了一个类型,目的是方便进行定义的时候代码的替代性强

typedef struct Seqlist
{
	SLDataType* arr;//首元素的地址,因为这里我们采取的动态开辟
	int size;//有效数据个数
	int capacity;//空间大小
}SL;

这里提醒一下,.c文件不能忘记包含头文件

3.罗列需要的函数(顺序表)

//开辟空间
void SLCheckCapacity(SL* ps);

//顺序表初始化
void SLInit(SL* ps);

//顺序表的销毁
void SLDestroy(SL* ps);

//打印
void SLprintf(SL ps);

//头部插入删除/尾部插入删除
void SLPushBack(SL* ps, SLDataType x);//尾部的插入
void SLPushFront(SL* ps, SLDataType x);//头部的插入

void SLPopBack(SL* ps);//尾部的删除
void SLPopFront(SL* ps);//头部的删除

//指定位置之前插入/删除数据
void SLInsert(SL* ps, int pos, SLDataType x);//指定位置之前插入数据
void SLErase(SL* ps, int pos);//删除指定位置的数据

// 指定位置的查找,因为需要返回所在的位置,所以需要用int的类型
int SLFind(SL* ps, SLDataType x);

4.函数的实现(顺序表初始化)

这里是顺序表的初始化。

解释一下,首先 顺序表的初始化也就是初始化我们创建的结构体,在我们创建的结构体里面我们可以看到

第一个是int类型的指针类型,指向的开辟空间的首元素的地址,我们此时还没有开辟空间,所以我们初始化的时候先指向空指针

int size;//有效数据个数,什么意思呢,也就是我们实际的使用空间的大小,当我们使用的时候才会占据

int capacity;//空间大小,我们开辟空间的大小,往往开辟的空间是按照倍数进行开辟的,所以就算是按照二倍进行创建,那么原来的空间的大小是4,扩容之后变成8,但是此时实际使用的可能还是4,所以这次是需要空间大小,和实际空间大小的目的

#include"seqlist.h"
//初始化
void SLInit(SL* ps)
{
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
	//指针指向null,空间和大小初初始化的时候都等于0
}

5.函数的实现(顺序表销毁)

这里我们先写出空间的销毁函数,因为简单,好理解。我们知道空间创建,使用完之后就需要进行销毁。

这个逻辑是什么呢

首先就是只要指向的空间是arr,并且此时不是空,也就是存在空间的情况下,我们才进行销毁,要是你不存在空间,直接进行销毁,又可能报错。 

最后也就是让两个空间大小和实际大小都为0.那么为了书写方便,我们直接简化了

//顺序表的销毁
void SLDestroy(SL* ps)
{
	if (ps->arr)
	{
		free(ps->arr);//释放指向的空间,所以我们这里二次了解到,arr才是空间,剩余两个一个是实际占用空间,一个是空间大小
	}
	ps->arr = NULL;
	ps->size = ps->capacity = 0;

//等价于
//	ps->size =0;
//  ps->capacity = 0;
}

5.函数的实现(顺序表空间的申请)

空间的申请,什么时候进行申请,申请用什么函数,我们逐步解答

动态内存函数的使用和综合实践-malloc,free,realloc,calloc-CSDN博客

首先就是申请用什么函数

用realloc函数,一张图直接明确的表述为什么是realloc函数,因为realloc函数是连续的,而malloc函数不是连续的,是链表里面使用的。

其次就是什么时候需要申请空间

那当然是实际空间不够用的时候,才需要申请空间,

空间申请的逻辑

在数学逻辑上,申请空间的时候我们往往是采取二倍或者四倍的方式进行空间的申请(偶数倍),这样不会导致空间不够使用

逻辑讲解 

这里我们首先是进行判断,是不是需要申请空间,如果是需要申请的话,我们再申请,不需要的话就直接跳出。 

realloc函数的特点是开辟新的空间赋值到新空间,并且销毁旧的空间。需要知道一点的是,只要是新空间的开辟就可能失败,所以我们防止失败,我们先进行新的参数接受开辟的空间,没有问题的情况下我们再进行赋值接受。

 三木操作符的目的是进行判断因为你申请的空间可能是0,那么如果是0的情况下,你再怎么乘也是0的空间大小,所以我们进行一个三目操作符的判断,当初始空间的大小是0的情况下,我们给四个整形的空间大小,当不为0的时候,我们把空间大小乘以二倍,赋值给变量。

然后realloc函数里面,这里扩容的大小是字节大小,所以需要再*类型的字节,这里你可以直接*4,但是这样的话代码的可替代性就弱了,我们直接是计算int的字节大小也就是

sizeof(int) 

//开辟空间
void SLCheckCapacity(SL* ps)
{
	//插入空间之前看看,空间是否够不够
	if (ps->capacity == ps->size)//当空间大小等于size的时候,size也就是实际占用的大小
	{
		//申请空间
		//realloc动态申请空间,void* realloc(void* ptr, size_t size);
		//申请的空间的大小一般是双倍进行申请空间
		//这里采取三目操作符,目的是判断当前的空间的大小是否为0,
		//如果是0的情况下,则此时说明哪怕开辟空间你*0了最后也是0,所以给定一个基础的4个整形的字节大小,也就是4
		//如果不等于0的情况下,可以继续进行双倍的空间的扩展
		//realloc申请空间的时候可能申请失败,所以我们利用一个变量进行接收

		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;//空间申请成功,然后赋值给结构体里面,空间申请成功,空间大小改变,为0变成4个类型,不为0变成双倍的类型
		ps->capacity = newCapacity;//同时我们改变实际的空间的大小,2 * ps->capacity;而我们开辟的时候开辟的是字节的大小,申请成功,空间大小改变,首元素的地址改变
	}
}

 6.函数的实现(顺序表的打印)

这里我们先写打印的函数,目的是方便观察

逻辑很简单你把这个顺序表当成 数组的打印你会发现一下就看懂了

起始条件是0,也就是数组的0下标,结束条件是实际的大小,因为只有实际的大小才有内容、注意观察打印这里我们没有采取指针的方式,所以.就可以,如果是指针 那就需要箭头了

//打印
void SLprintf(SL ps)
{
	for (int i = 0; i < ps.size; i++)
	{
		printf("%d ", *(ps.arr + i));//这里需要进行解应用,因为这里传递的是首元素地址,解应用之后才是元素
	}
	printf("\n");
}

 7.函数的实现(顺序表的头部的插入)

首先我们看看头插是插入到哪里,显而易见,我们是在开头插入一个元素

所以我们需要什么

1.先判断空间够不够(当实际空间等于使用空间的时候,自然会扩展,当实际空间是4的时候,使用是3的时候,刚好还可以插入一个空间,而开始创建的空间我们就给了四个整形类型的空间)

2.整体往后移动一位(也就是下标是0的往后走,变成下标是1的数值,以此类推)

3,把数值插入到头部(那么此时也就是让ps->arr[0]=x;)也就是把需要插入的数值插入到顺序里面

4.实际大小需要++,(最后我们的原来空间是4,占据3,那么此时我们++,变成实占据的空间也变成4,那么下一次进行开辟空间的时候只要调用申请空间,自然会扩容)

5.进行断言,不能传递空指针

//头部插入
void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);
	SLCheckCapacity(ps);//开辟空间函数
	//这里的目的是让顺序表的整体的空间往后移动一位
	for (int i = ps->size; i > 0; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[0] = x;//arr[0]插入首元素的地址,因为已经把后面的数值向后移动了
	ps->size++;//这里因为已经插入,所以size实际占用的空间向后移动,在之后的开辟的时候,如果等于,然后再继续开辟
}

 8.函数的实现(顺序表的尾部的插入)

尾部的插入的逻辑我们来看看,也就顾名思义,最后一个空间插入数值

所以我们需要什么

那么此时就很简单了

1.判断空间够不够

2.够的话让ps->arr[ps->size]=x;把最后一个空间输入数值,然后再把使用的空间++就可以

(这里说明一下,这里是arr的size的下标,也就是如果是0下标需要插入尾插,那么size=1,也就是在第二个元素进行插入,这里不要搞混)

3.不够的话开辟空间,然后循环操作

 SLDataType x(需要插入的数值)

//尾部插入
void SLPushBack(SL* ps, SLDataType x)
{
	assert(ps);//这里进行一个断言,防止传的时候是一个空指针
	SLCheckCapacity(ps);//开辟空间
	ps->arr[ps->size] = x;
	ps->size++;//这里是实际的大小,也是就在空间大小和实际占据内存多少进行对比的时候需要的

	//ps->arr[ps->size++] = x;//也可以写成这个模式

}

 9.函数的实现(顺序表的尾部的删除)

尾部的删除是很简单的

所以我们需要什么

1.size--(我们只需要舍弃尾部的数据就可以了,也就是把实际空间减少一个 ,就尾部删除了一个,所以也就是我们连赋值都可以舍弃,赋值是为了防止越界的时候,搞混淆,你不小心越界了,这个数值的空间地址还没使用,我们很尴尬,可能会)

2.结束,并且实际使用空间大小--

//尾部的删除
void SLPopBack(SL* ps)
{
	assert(ps);//首先是传参的指针不为空
	assert(ps->size);//其次是删除的时候,里面是有实际的内容的,不是空的,也就是顺序表不为空
	//ps->arr[ps->size - 1]=-1;//这里是赋值了,其实也可以不赋值,因为是直接舍弃这个空间
	ps->size--;
}

 10.函数的实现(顺序表的头部的删除)

头部的删除可能会稍微比尾部的删除复杂一点,但是也只是那么一点点

所以我们需要什么

1.我们只需要从前往后进行覆盖就可以

2.实际使用的空间--

//头部的删除
void SLPopFront(SL* ps)
{
	assert(ps);//首先是传参的指针不为空
	assert(ps->size);//其次是删除的时候,里面是有实际的内容的,不是空的,也就是顺序表不为空
	//这里的逻辑就是整体往后移动一位,也就是让后一个数值直接覆盖前一个数值,最后让实际空间大小--一个就可以
	for (int i = 0; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}

12.函数的实现(顺序表的指定位置插入)

这里有一点难度因为我们需要找到指定的位置,我们插入数值,所以我们的函数的参数也就是三个


//指定前位置插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);//这里插入的时候,插入的位置需要在实际内存里面,pos指定的是指定位置
	SLCheckCapacity(ps);//判断空间够不够
	//这里不能从前往后进行移动,因为会导致数值的覆盖,只能从后往前进行覆盖
	for (int i = ps->size; i > pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos] = x;
	ps->size++;
}

我们需要什么

1.判断空间够不够

2.找到需要插入的下标的位置

3.循环移动之后的数值,这里需要注意这里数值的循环覆盖只能是从size开始,--到pos也就是我们需要的所在位置的下标,因为会导致覆盖,也就是1 2 3 4,从下标为1的数值前进行插入,所以如果是从前往后进行覆盖,会导致,1 【插入数值】 2 2 2.

4.最后size++,(实际占据的空间大小++)

测试一下没有问题

13.函数的实现(顺序表的指定位置删除)

指定位置的删除的关键点是找到位置,然后进行覆盖,很简单

需要注意最后不要忘记实际空间剩余--,进行计数 

//删除指定位置的数据
void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);//依旧是有效区间
	for (int i = pos; i < ps->size - 1; i++)//这里必须进行-1,因为最后是ps->arr[size]=ps->arr[size+1];会导致越界访问
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}

14.函数的实现(顺序表的指定位置的查找)

这里就非常简单了,只需要循环遍历就可以了

//指定位置的查找
int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size); 
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] = x)
		{
			//找到了
			return i;
		}
	}
	//没有找到
	return -1;
}

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

顺序表的代码

test.c

//顺序表的测试
#include"seqlist.h"
void SLTest01()
{
	SL s1;
	//初始化
	SLInit(&s1);
	//测试尾插
	SLPushBack(&s1, 1);
	SLprintf(s1);
	SLPushBack(&s1, 2);
	SLprintf(s1);
	SLPushBack(&s1, 3);
	SLprintf(s1);
	SLPushBack(&s1, 4);
	SLprintf(s1);
	//测试头插
	SLPushFront(&s1, 9);
	SLprintf(s1);
	SLPushFront(&s1, 8);
	SLprintf(s1);
	SLPushFront(&s1, 7);
	SLprintf(s1);
	SLPushFront(&s1, 6);
	SLprintf(s1);
	//尾部的删除测试
	SLPopBack(&s1);
	SLprintf(s1);
	SLPopBack(&s1);
	SLprintf(s1);
	SLPopBack(&s1);
	SLprintf(s1);
	SLPopBack(&s1);
	SLprintf(s1);
	//头部的删除测试
	SLPopFront(&s1);
	SLprintf(s1);
	SLPopFront(&s1);
	SLprintf(s1);
	SLPopFront(&s1);
	SLprintf(s1);
	//指定位置的插入
	SLInsert(&s1, 0, 0);
	SLprintf(s1);
	SLInsert(&s1, s1.size, 99);
	SLprintf(s1);
	SLInsert(&s1, s1.size, 88);
	SLprintf(s1);
	SLInsert(&s1, s1.size, 77);
	SLprintf(s1);
	//删除指定位置的数据
	SLErase(&s1, 1);
	SLprintf(s1);
	SLErase(&s1, 0);
	SLprintf(s1);
	//指定位置的查找
	int ret1 = SLFind(&s1, 0);
	if (ret1 != -1)
	{
		printf("找到了所在位置是:%d\n", ret1);
	}
	else
	{
		printf("没找到。\n");

	}
	int ret2 = SLFind(&s1, 99);
	if (ret2 != -1)
	{
		printf("找到了所在位置是:%d\n", ret2);
	}
	else
	{
		printf("没找到。\n");

	}

}
int main()
{
	SLTest01();
	return 0;
}

seqlist.h

//顺序表的头文件
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#define N 100
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include"contact.h"//因为头文件是不能互相包含的,所以我们这里采取顺序表包含通讯录的头文件
//这样我们调用顺序表的时候,自然也就是调用了通讯录的头文件

//但是此时我们还需要进行前置声明,因为头文件是不能相互包含的,但是我们还需要顺序表里面的内容


typedef int SLDataType;//这里定义了一个类型,目的是方便进行定义的时候代码的替代性强
//typedef peoInfo SLDataType;//这里定义了一个类型,目的是方便进行定义的时候代码的替代性强

typedef struct Seqlist
{
	SLDataType* arr;//首元素的地址,因为这里我们采取的动态开辟
	int size;//有效数据个数
	int capacity;//空间大小
}SL;//typedef struct Seqlist SL;//结构体的重命名

//开辟空间
void SLCheckCapacity(SL* ps);

//顺序表初始化
void SLInit(SL* ps);

//顺序表的销毁
void SLDestroy(SL* ps);

//打印
void SLprintf(SL ps);

//头部插入删除/尾部插入删除
void SLPushBack(SL* ps, SLDataType x);//尾部的插入
void SLPushFront(SL* ps, SLDataType x);//头部的插入

void SLPopBack(SL* ps);//尾部的删除
void SLPopFront(SL* ps);//头部的删除

//指定位置之前插入/删除数据
void SLInsert(SL* ps, int pos, SLDataType x);//指定位置之前插入数据
void SLErase(SL* ps, int pos);//删除指定位置的数据

// 指定位置的查找,因为需要返回所在的位置,所以需要用int的类型
int SLFind(SL* ps, SLDataType x);


seqlist.c

//顺序表的实现
#include"seqlist.h"
//初始化
void SLInit(SL* ps)
{
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
	//指针指向null,空间和大小初初始化的时候都等于0
}

//顺序表的销毁
void SLDestroy(SL* ps)
{
	if (ps->arr)
	{
		free(ps->arr);//释放指向的空间
	}
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}

//打印
void SLprintf(SL ps)
{
	for (int i = 0; i < ps.size; i++)
	{
		printf("%d ", *(ps.arr + i));//这里需要进行解应用,因为这里传递的是首元素地址,解应用之后才是元素
	}
	printf("\n");
}

//开辟空间
void SLCheckCapacity(SL* ps)
{
	//插入空间之前看看,空间是否够不够
	if (ps->capacity == ps->size)//当空间大小等于size的时候,size也就是实际占用的大小
	{
		//申请空间
		//realloc动态申请空间,void* realloc(void* ptr, size_t size);
		//申请的空间的大小一般是双倍进行申请空间
		//这里采取三目操作符,目的是判断当前的空间的大小是否为0,
		//如果是0的情况下,则此时说明哪怕开辟空间你*0了最后也是0,所以给定一个基础的4个整形的字节大小,也就是4
		//如果不等于0的情况下,可以继续进行双倍的空间的扩展
		//realloc申请空间的时候可能申请失败,所以我们利用一个变量进行接收

		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;//同时我们改变实际的空间的大小,2 * ps->capacity;而我们开辟的时候开辟的是字节的大小
	}

}

//尾部插入
void SLPushBack(SL* ps, SLDataType x)
{
	assert(ps);//这里进行一个断言,防止传的时候是一个空指针
	SLCheckCapacity(ps);//开辟空间
	ps->arr[ps->size] = x;
	ps->size++;//这里是实际的大小,也是就在空间大小和实际占据内存多少进行对比的时候需要的

	//ps->arr[ps->size++] = x;//也可以写成这个模式

}

//头部插入
void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);
	SLCheckCapacity(ps);//开辟空间
	//这里的目的是让顺序表的整体的空间往后移动一位
	for (int i = ps->size; i > 0; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[0] = x;//arr[0]插入首元素的地址,因为已经把后面的数值向后移动了
	ps->size++;//这里因为已经插入,所以size实际占用的空间向后移动,在之后的开辟的时候,如果等于,然后再继续开辟
}

//尾部的删除
void SLPopBack(SL* ps)
{
	assert(ps);//首先是传参的指针不为空
	assert(ps->size);//其次是删除的时候,里面是有实际的内容的,不是空的,也就是顺序表不为空
	ps->arr[ps->size - 1] = -1;//这里是赋值了,其实也可以不赋值,因为是直接舍弃这个空间
	ps->size--;
}

//头部的删除
void SLPopFront(SL* ps)
{
	assert(ps);//首先是传参的指针不为空
	assert(ps->size);//其次是删除的时候,里面是有实际的内容的,不是空的,也就是顺序表不为空
	//这里的逻辑就是整体往后移动一位,也就是让后一个数值直接覆盖前一个数值,最后让实际空间大小--一个就可以
	for (int i = 0; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}


//指定前位置插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);//这里插入的时候,插入的位置需要在实际内存里面,pos指定的是指定位置
	SLCheckCapacity(ps);//判断空间够不够
	for (int i = ps->size; i > pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos] = x;
	ps->size++;
}

//删除指定位置的数据
void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);//依旧是有效区间
	for (int i = pos; i < ps->size - 1; i++)//这里必须进行-1,因为最后是ps->arr[size]=ps->arr[size+1];会导致越界访问
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}

//指定位置的查找
int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] = x)
		{
			//找到了
			return i;
		}
	}
	//没有找到
	return -1;
}