顺序表(增删减改)+通讯录项目(数据结构)

什么是顺序表

顺序表和数组的区别

顺序表本质就是数组

结构体初阶+进阶 系统化的学习-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;
}

通讯录项目

通讯录的实现逻辑

通讯录的实现逻辑可以分为几个主要部分:数据结构设计、数据存储、数据检索、数据更新和用户界面。下面我会逐一解释这些部分。

1. 数据结构设计

通讯录的基本数据结构通常是列表或数组,用于存储联系人的信息。每个联系人可以包含如下信息:

  • 姓名
  • 电话号码
  • 电子邮件
  • 地址
  • 备注

在编程中,这些信息可以存储在结构体(如C语言)或者类(如Java、Python)中。

2. 数据存储

数据存储涉及到将联系人信息保存在某种数据存储媒介中。这可以是一个文件、数据库或云服务。

  • 文件存储:简单地将数据保存在文本文件或二进制文件中。
  • 数据库存储:使用数据库管理系统(如MySQL、SQLite),可以提供更为复杂的数据查询和管理功能。
  • 云存储:如使用云服务提供商(如阿里云、腾讯云)提供的存储解决方案,可以实现数据的远程访问和共享。

3. 数据检索

数据检索是指当用户需要查找联系人时,系统能够快速地找到并显示相关信息。这通常涉及到编写查询算法,根据姓名或其他关键词来检索数据。

4. 数据更新

数据更新包括增加新的联系人、修改现有联系人的信息或删除联系人。系统需要提供安全的机制来防止数据被未授权的用户修改。

5. 用户界面

用户界面是用户与通讯录程序交互的界面。它可以是命令行界面,也可以是图形用户界面(GUI),提供直观的按钮、菜单和输入框,使用户能够方便地添加、查找和编辑联系人。

通讯录和顺序表之间的关系

通讯录和顺序表之间的关系在于通讯录的数据结构可以由顺序表来实现。

顺序表是一种线性数据结构,其中元素按顺序存储,并且可以通过索引直接访问。通讯录是一个组织联系人信息的系统,而顺序表提供了一种存储这些信息的方式。

在编程中,通讯录通常包含一系列的联系人类型对象,这些对象包含了联系人的详细信息,如姓名、电话号码、电子邮件地址等。这些联系人类型的对象可以被存储在顺序表中,这样每个联系人都占据表中的一个位置,可以通过索引来访问。

逻辑图

首先我们来看看逻辑图

也可以理解为

或者理解为这样

最后可以理解为这样

总的来说是线性的


通讯录的实现逻辑

通讯录实现的白话解释

1.依旧是头文件和实现文件的使用

2.因为通讯录的使用是需要涉及链表的,但是又不能和链表相互包含头文件的,所以我们需要进行前置声明结构体-前置声明-CSDN博客

3.和顺序表一样,但是因为是调用顺序表,而且是自定义类型,这就会导致之前的自定义的int类型是不能继续使用的,所以我们需要删除一些代码,最简单的方式就是直接注释

4.通讯录的函数声明

//通讯录的初始化
//通讯录的初始化实际上是顺序表的初始化
void ContactInit(Contact* con);

//通讯录的销毁
void ContactDesTory(Contact* con);

//通讯录的增加
void ContactAdd(Contact* con);

//通讯录的删除
void ContactDel(Contact* con);

//通讯录的修改
void ContactInit(Contact* con);

//通讯录的修改
void ContactModify(Contact* con);

//展示通讯录
void ContactShow(Contact* con);

//通讯录的查找这里是查找名字,这里的目的是在函数内部进行查找,然后进行删除,其实可以不用进行声明
int FindByname(Contact* con, char name[]);

//通讯录的查找
void ContactFind(Contact* con);

//写入到文件里面
void SaveContact(Contact* con);


通讯录的实现

1.创建头文件(通讯录)

这里我们采取的宏定义的方式定义初始化的头文件,同时我们进行重命名,命名为peoInfo;

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once//这里的意思是只能定义一次
#define NAME_MAX 20//姓名最大容量
#define GENDER_MAX 10//性别最大容量
#define TEL_MAX 20//电话最大容量
#define ADDR_MAX 20//地址最大容量

//定义通讯录需要的内容,联系人的数据结构
//姓名,性别,年龄,电话,地址
typedef struct personInfor
{
	char name[NAME_MAX];//姓名
	char gender[GENDER_MAX];//性别
	int age;//年龄
	char tel[TEL_MAX];//电话
	char addr[ADDR_MAX];//地址
}peoInfo;

2.前置声明(关键一步)

结构体-前置声明-CSDN博客

前置什么的目的是什么呢,这里的目的是,因为我们的头文件是不能相互包含的,而且通讯录的本质其实就是链表,链表的本质就是数组。

那我们调用顺序表的时候我们不能直接在contact.c里面直接使用SL,因为我们会导致混淆,所以为了防止混淆,我们需要把顺序表在通讯录,c文件里面,把顺序表进行重命名。顺序表命名为Contact。

同时我们在顺序表定义的结构体里面自定义的类型可以改为结构体类型,并且直线首元素的地址。

所以我们可以看到

Contact.h文件

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#define NAME_MAX 20//姓名最大容量
#define GENDER_MAX 10//性别最大容量
#define TEL_MAX 20//电话最大容量
#define ADDR_MAX 20//地址最大容量

//定义通讯录需要的内容,联系人的数据结构
//姓名,性别,年龄,电话,地址
typedef struct personInfor
{
	char name[NAME_MAX];//姓名
	char gender[GENDER_MAX];//性别
	int age;//年龄
	char tel[TEL_MAX];//电话
	char addr[ADDR_MAX];//地址
}peoInfo;

//前置声明,因为头文件是不能相互包含的,但是我们还需要顺序表里面的内容
//前置声明的意义是允许我们在声明变量之前先告诉编译器结构体的名字,而不是先给出内容
// 这里的目的是typedef SL Contact;编译器是不能判断的,
// 因为我们在顺序表里面定义的我们的内容,而且没有包含我们的顺序表里面的头文件,所以我们需要进行前置声明
//struct SL Contact;
//对链表重命名->通讯录,因为通讯录的本质就是链表,所以这里为了防止冲突把顺序表的名字改一下,变成通讯录
//但是这个会产生的问题就是,这里没有相互包含头文件,所以需要的是进行前置声明
//这里SL是不行的,SL是定义之后改名字的,所以我们需要直接用 Seqlist
//然后重命名为通讯录
typedef struct Seqlist Contact;

typedef struct Seqlist Contact;这里进行了前置声明

//这里SL是不行的,SL是定义之后改名字的,所以我们需要直接用 Seqlist

seqlist.h文件里面

所以这里就进行了前置声明,也就是我们在通讯录头文件包含了顺序表的头文件,但是顺序表的头文件不包含通讯录的头文件,但是可以使用顺序表里面的名字

typedef int SLDataType;//这里定义了一个类型,目的是方便进行定义的时候代码的替代性强,,这里是把之前定义的int类型改为自定义类型也就是,名字,性别,年龄,电话,地址

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


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

typedef peoInfo SLDataType;//这里定义了一个类型,目的是方便进行定义的时候代码的替代性强
//这里的头文件以及进行包含了 pepinfo以及的定义的自定义的类型所以我们下面的结构体指向的是首元素的地址

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

所以我们就可以把这俩头文件进行合并就可以理解为

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#define NAME_MAX 20//姓名最大容量
#define GENDER_MAX 10//性别最大容量
#define TEL_MAX 20//电话最大容量
#define ADDR_MAX 20//地址最大容量
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
//定义通讯录需要的内容,联系人的数据结构
//姓名,性别,年龄,电话,地址
typedef struct personInfor
{
	char name[NAME_MAX];//姓名
	char gender[GENDER_MAX];//性别
	int age;//年龄
	char tel[TEL_MAX];//电话
	char addr[ADDR_MAX];//地址
}peoInfo;

//前置声明,因为头文件是不能相互包含的,但是我们还需要顺序表里面的内容
//前置声明的意义是允许我们在声明变量之前先告诉编译器结构体的名字,而不是先给出内容
// 这里的目的是typedef SL Contact;编译器是不能判断的,
// 因为我们在顺序表里面定义的我们的内容,而且没有包含我们的顺序表里面的头文件,所以我们需要进行前置声明
//struct SL Contact;
//对链表重命名->通讯录,因为通讯录的本质就是链表,所以这里为了防止冲突把顺序表的名字改一下,变成通讯录
//但是这个会产生的问题就是,这里没有相互包含头文件,所以需要的是进行前置声明
//这里SL是不行的,SL是定义之后改名字的,所以我们需要直接用 Seqlist
//然后重命名为通讯录


typedef struct Seqlist Contact;






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

typedef peoInfo SLDataType;//这里定义了一个类型,目的是方便进行定义的时候代码的替代性强
//这里的头文件以及进行包含了 pepinfo以及的定义的自定义的类型所以我们下面的结构体指向的是首元素的地址

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

这里空白的部分就是上下文件的分界

3.头文件函数的声明

//通讯录的初始化
//通讯录的初始化实际上是顺序表的初始化
void ContactInit(Contact* con);

//通讯录的销毁
void ContactDesTory(Contact* con);

//通讯录的增加
void ContactAdd(Contact* con);

//通讯录的删除
void ContactDel(Contact* con);

//通讯录的修改
void ContactInit(Contact* con);

//通讯录的修改
void ContactModify(Contact* con);

//展示通讯录
void ContactShow(Contact* con);

//通讯录的查找这里是查找名字,这里的目的是在函数内部进行查找,然后进行删除,其实可以不用进行声明
int FindByname(Contact* con, char name[]);

//通讯录的查找
void ContactFind(Contact* con);

//写入到文件里面
void SaveContact(Contact* con);

4.通讯录的初始化

实际就是直接调用顺序表进行初始化就可以

//通讯录的初始化
//通讯录的初始化实际上是顺序表的初始化
void ContactInit(Contact* con)
{
	SLInit(con);//这里传递首元素的地址就可以,不需要传递自定义结构体
}

5.通讯录的销毁

同理

//通讯录的销毁
void ContactDesTory(Contact* con)
{
	SLDestroy(con);
}

6.通讯录的增加

通讯录的增加是需要进入到ps->arr.里面进行输入的,并且在输入之前是需要进行判断空间够不够的。

插入的方式有很多种,这里我们采取的是尾部插入的方式,根据顺序表来说的话,其实你可以指定位置插入等等,都是可以的,但是尾部插入是最合适的。

//通讯录的增加
void ContactAdd(Contact* con)
{
	peoInfo info;//已有方式的复用,创建一个变量,可以输入信息,然后存储到内存空间里面
	printf("输入需要添加的联系人姓名:\n"); 
	scanf("%s", info.name);//把获取到的信息保存到结构体里面

	printf("输入需要添加的联系人性别:\n");
	scanf("%s", info.gender);

	printf("输入需要添加的联系人年龄:\n");
	scanf("%d", &info.age);//scanf需要取地址,因为前面的几个是首元素的地址,本身就是地址

	printf("输入需要添加的联系人电话:\n");
	scanf("%s", info.tel);

	printf("输入需要添加的联系人地址:\n");
	scanf("%s", info.addr);

	SLPushBack(con, info);//尾部的插入
}

这里讲解一下contact con;这里是创建一个通讯录的对象,不然是没有参数的, 这里你可以是空的,但是只要是有这个类型,就像

int x;scanf("%d",&x);后面进行函数调用的时候会进行改变,这里是创建通讯录的对象,是顺序表的机构,然后arr里面是自定义类型。给出了一个足够大的空间。不管里面是什么,有没有初始化,至少空间有了,那么你进行增删减改的时候,就有的动。

自定义类型已有方式的复用

复用自定义类型的方法通常包括:

  1. 继承:在面向对象编程语言中,可以通过继承来复用自定义类型。子类继承父类,并可以添加或修改其行为。

  2. 组合:将自定义类型作为其他类型的成员,这样可以复用已有的自定义类型来构建更复杂的数据结构。

  3. 接口:定义一个接口(在面向对象编程中),然后让自定义类型实现这个接口。这样,任何符合该接口的自定义类型都可以被当作是同一类型的对象来使用。

  4. 泛型:在支持泛型的编程语言中,可以使用泛型来创建可以接受任何类型的自定义类型,从而复用代码。

  5. 宏定义:在某些编程语言中,可以使用宏定义来创建自定义类型的简化版本或别名,从而减少重复的代码。

  6. 封装:将自定义类型的实现细节隐藏起来,只暴露公共接口。这样,其他代码可以使用这个自定义类型而不需要了解其内部实现。

  7. 库或模块:将使用自定义类型的代码封装在一个库或模块中,这样可以在不同的项目中复用这个自定义类型。

7.通讯录的删除

通讯录的删除本质还是顺序表,这里我们发现其实通讯录的实现本质还是顺序表的实现

那么我们的逻辑是删除,那么应该删除的有什么,什么为基准,我们可以是名字进行删除,那就要先进行查找,找到名字或者手机号,然后删除全部的东西,所以我们先完善查找的逻辑

这里是strcmp进行对比,为了方便书写,对比名字是不是一样,这里采取strcmp函数,C语言-strcmp(对比函数模拟和使用)-CSDN博客,只要相等返回的肯定是0.所以我们返回这个数值的下标,记得是下标,因为后续我们还需要进行删除,我们需要直接删除这些下标的东西,而不是只是这个名字。

//通讯录的查找这里是查找名字
int FindByname(Contact* con, char name[])
{
	for (int i = 0; i < con->size; i++)
	{
		if (0 == strcmp(con->arr[i].name, name))
		{
			return i;//找到了,返回所在位置的下标
		}
	}
	return -1;//没找到
}

所以删除代码里面我们可以看到,因为这里是线性的,所以这个下标也就是整个这个结构的下标,所以我们可以直接删除这个结构,

//通讯录的删除
void ContactDel(Contact* con)
{
	//删除数据,需要查找到才能删除数据
	char name[NAME_MAX];
	printf("输入删除联系人的姓名:\n");
	scanf("%s", name);
	int find = FindByname(con, name);
	if (find == -1)
	{
		printf("你需要删除的联系人不存在。\n");
		return 1;
	}
	SLErase(con, find);//删除指定位置的数据
	printf("删除成功。\n");
}

我这里把顺序表的删除拿出来, 可以看到,是直接删除arr【i】下标

可以看见,所以进行了删除

8.通讯录的修改

通讯录的修改,还是需要找到联系人的才能修改,要是联系人不存在,那么修改我没有意义的,所以我们查找联系人在不在,然后修改每一次的需要修改的下标,我们直接进行覆盖

//通讯录的查找这里是查找名字
int FindByname(Contact* con, char name[])
{
	for (int i = 0; i < con->size; i++)
	{
		if (0 == strcmp(con->arr[i].name, name))
		{
			return i;//找到了,返回所在位置的下标
		}
	}
	return -1;//没找到
}

//通讯录的修改
void ContactModify(Contact* con)
{
	char name[NAME_MAX];
	printf("输入修改的联系人姓名:\n");
	scanf("%s", name);
	int find = FindByname(con, name);
	if (find == -1)
	{
		printf("你需要修改的联系人不存在。\n");
		return 1;
	}
	//直接进行覆盖
	printf("输入需要添加的联系人姓名:\n");
	scanf("%s", con->arr[find].name);

	printf("输入需要添加的联系人性别:\n");
	scanf("%s", con->arr[find].gender);

	printf("输入需要添加的联系人年龄:\n");
	scanf("%s", &con->arr[find].age);//这里需要取地址

	printf("输入需要添加的联系人电话:\n");
	scanf("%s", con->arr[find].tel);

	printf("输入需要添加的联系人地址:\n");
	scanf("%s", con->arr[find].addr);

}

9.展示通讯录

这里的展示其实就是逐步打印出来

//展示通讯录
void ContactShow(Contact* con)
{
	printf("%s %s %s %s %s", "姓名", "性别", "年龄", "电话", "地址\n");
	for (int i = 0; i < con->size; i++)
	{
		printf("%s\t %s\t %d\t %s\t %s\t\n",
			con->arr[i].name,
			con->arr[i].gender,
			con->arr[i].age,
			con->arr[i].tel,
			con->arr[i].addr
		);
	}
}

先找到结构体动态顺序表的arr的首元素的地址,然后再第i个里去寻找名字,性别,年龄,电话,地址,然后循环总体是con->size个,也就是实际个数

10.通讯录的查找这里是查找名字

//通讯录的查找这里是查找名字
int FindByname(Contact* con, char name[])
{
	for (int i = 0; i < con->size; i++)
	{
		if (0 == strcmp(con->arr[i].name, name))
		{
			return i;//找到了,返回所在位置的下标
		}
	}
	return -1;//没找到
}

这里是找到之后打印你需要的,找不到就打印找不大,和名字查找的逻辑其实是一样的,没有什么难度

11.通讯录的查找

//通讯录的查找
void ContactFind(Contact* con)
{
	char name[NAME_MAX];
	printf("输入需要查找的名字:\n");
	scanf("%s", name);
	int find = FindByname(con, name);
	if (find == -1)
	{
		printf("你需要修改的联系人不存在。\n");
		return 1;
	}
	ContactShow(con);
}

这里是找到之后打印你需要的,找不到就打印找不大,和名字查找的逻辑其实是一样的,没有什么难度

12.写入到文件里面

C语言-文件操作函数进阶+printf+scanf+sscanf+sprintf+fprintf+fscanf+fwrite+fread+fseek+ftell+rewind+feof-CSDN博客

为了可以让通讯录长时间的使用,我们可以把文件输入之后,保存在文件里面,这里我们保存的是txt文件,并且创建文件之后进行销毁

//写入到文件里面
void  SaveContact(Contact* con)
{
	FILE* pf = fopen("通讯录.txt", "w");
	if (pf==NULL)
	{
		perror("fopen error:\n");
		return;
	}
	//写入到文件里面
	for (int i = 0; i < con->size; i++)
	{
		//fwrite(con->arr, sizeof(peoInfo), 1, pf);//二进制的形式写进去
		fprintf(pf, "%s %s %d %s %s",
			con->arr[i].name,
			con->arr[i].gender,
			con->arr[i].age,
			con->arr[i].tel,
			con->arr[i].addr
		);//文本的形式写进去
	}
	fclose(pf);
	pf = NULL;
	printf("保存成功。\n");
}

13.测试函数的实现

很简单,不过多强调这个

void menu()
{
	printf("********************通讯录********************\n");
	printf("*     1, 增加联系人     2,删除联系人        *\n");
	printf("*     3,修改联系人     4,查找联系人        *\n");
	printf("*     5,展示联系人     6,存储到文件        *\n");
	printf("********************0退出*********************\n");
}
int main()
{
	//SLTest01();
	//Contact01();
	int input = 1;
	Contact con;
	ContactInit(&con);
	do
	{
		menu();
		printf("请选择您的操作:\n");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			ContactAdd(&con);
			break;
		case 2:
			ContactDel(&con);
			break;
		case 3:
			ContactModify(&con);
			break;
		case 4:
			ContactFind(&con);
			break;
		case 5:
			ContactShow(&con);
			break;
		case 6:
			SaveContact(&con);
			break;
		case 0:
			printf("退出通讯录。");
			break;
		default:
			printf("请输入正确的修改数字。");
			break;
		}

	} while (input!=0);
	return 0;
}

通讯录实现的代码

顺序表的代码

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;
}

contact.h

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#define NAME_MAX 20//姓名最大容量
#define GENDER_MAX 10//性别最大容量
#define TEL_MAX 20//电话最大容量
#define ADDR_MAX 20//地址最大容量

//定义通讯录需要的内容,联系人的数据结构
//姓名,性别,年龄,电话,地址
typedef struct personInfor
{
	char name[NAME_MAX];//姓名
	char gender[GENDER_MAX];//性别
	int age;//年龄
	char tel[TEL_MAX];//电话
	char addr[ADDR_MAX];//地址
}peoInfo;

//前置声明,因为头文件是不能相互包含的,但是我们还需要顺序表里面的内容
//前置声明的意义是允许我们在声明变量之前先告诉编译器结构体的名字,而不是先给出内容
// 这里的目的是typedef SL Contact;编译器是不能判断的,
// 因为我们在顺序表里面定义的我们的内容,而且没有包含我们的顺序表里面的头文件,所以我们需要进行前置声明
//struct SL Contact;
//对链表重命名->通讯录,因为通讯录的本质就是链表,所以这里为了防止冲突把顺序表的名字改一下,变成通讯录
//但是这个会产生的问题就是,这里没有相互包含头文件,所以需要的是进行前置声明
//这里SL是不行的,SL是定义之后改名字的,所以我们需要直接用 Seqlist
//然后重命名为通讯录
typedef struct Seqlist Contact;

//通讯录的初始化
//通讯录的初始化实际上是顺序表的初始化
void ContactInit(Contact* con);

//通讯录的销毁
void ContactDesTory(Contact* con);

//通讯录的增加
void ContactAdd(Contact* con);

//通讯录的删除
void ContactDel(Contact* con);

//通讯录的修改
void ContactInit(Contact* con);

//通讯录的修改
void ContactModify(Contact* con);

//展示通讯录
void ContactShow(Contact* con);

//通讯录的查找这里是查找名字,这里的目的是在函数内部进行查找,然后进行删除,其实可以不用进行声明
int FindByname(Contact* con, char name[]);

//通讯录的查找
void ContactFind(Contact* con);

//写入到文件里面
void  SaveContact(Contact* con);

contact.c

#include"seqlist.h"
#include"contact.h"
//通讯录的初始化
//通讯录的初始化实际上是顺序表的初始化
void ContactInit(Contact* con)
{
	SLInit(con);//这里传递首元素的地址就可以,不需要传递自定义结构体
}

//通讯录的销毁
void ContactDesTory(Contact* con)
{
	SLDestroy(con);
}
 
//通讯录的增加
void ContactAdd(Contact* con)
{
	peoInfo info;
	printf("输入需要添加的联系人姓名:\n"); 
	scanf("%s", info.name);//把获取到的信息保存到结构体里面

	printf("输入需要添加的联系人性别:\n");
	scanf("%s", info.gender);

	printf("输入需要添加的联系人年龄:\n");
	scanf("%d", &info.age);//scanf需要取地址,因为前面的几个是首元素的地址,本身就是地址

	printf("输入需要添加的联系人电话:\n");
	scanf("%s", info.tel);

	printf("输入需要添加的联系人地址:\n");
	scanf("%s", info.addr);

	SLPushBack(con, info);//尾部的插入
}
//通讯录的查找这里是查找名字
int FindByname(Contact* con, char name[])
{
	for (int i = 0; i < con->size; i++)
	{
		if (0 == strcmp(con->arr[i].name, name))
		{
			return i;//找到了,返回所在位置的下标
		}
	}
	return -1;//没找到
}

//通讯录的删除
void ContactDel(Contact* con)
{
	//删除数据,需要查找到才能删除数据
	char name[NAME_MAX];
	printf("输入删除联系人的姓名:\n");
	scanf("%s", name);
	int find = FindByname(con, name);
	if (find == -1)
	{
		printf("你需要删除的联系人不存在。\n");
		return 1;
	}
	SLErase(con, find);//删除指定位置的数据
	printf("删除成功。\n");
}

//展示通讯录
void ContactShow(Contact* con)
{
	printf("%s %s %s %s %s", "姓名", "性别", "年龄", "电话", "地址\n");
	for (int i = 0; i < con->size; i++)
	{
		printf("%s\t %s\t %d\t %s\t %s\t\n",
			con->arr[i].name,
			con->arr[i].gender,
			con->arr[i].age,
			con->arr[i].tel,
			con->arr[i].addr
		);
	}
}

//通讯录的修改
void ContactModify(Contact* con)
{
	char name[NAME_MAX];
	printf("输入修改的联系人姓名:\n");
	scanf("%s", name);
	int find = FindByname(con, name);
	if (find == -1)
	{
		printf("你需要修改的联系人不存在。\n");
		return 1;
	}
	//直接进行覆盖
	printf("输入需要添加的联系人姓名:\n");
	scanf("%s", con->arr[find].name);

	printf("输入需要添加的联系人性别:\n");
	scanf("%s", con->arr[find].gender);

	printf("输入需要添加的联系人年龄:\n");
	scanf("%s", &con->arr[find].age);//这里需要取地址

	printf("输入需要添加的联系人电话:\n");
	scanf("%s", con->arr[find].tel);

	printf("输入需要添加的联系人地址:\n");
	scanf("%s", con->arr[find].addr);

}

//通讯录的查找
void ContactFind(Contact* con)
{
	char name[NAME_MAX];
	printf("输入需要查找的名字:\n");
	scanf("%s", name);
	int find = FindByname(con, name);
	if (find == -1)
	{
		printf("你需要修改的联系人不存在。\n");
		return 1;
	}
	ContactShow(con);
}

//写入到文件里面
void  SaveContact(Contact* con)
{
	FILE* pf = fopen("通讯录.txt", "w");
	if (pf==NULL)
	{
		perror("fopen error:\n");
		return;
	}
	//写入到文件里面
	for (int i = 0; i < con->size; i++)
	{
		//fwrite(con->arr, sizeof(peoInfo), 1, pf);//二进制的形式写进去
		fprintf(pf, "%s %s %d %s %s",
			con->arr[i].name,
			con->arr[i].gender,
			con->arr[i].age,
			con->arr[i].tel,
			con->arr[i].addr
		);//文本的形式写进去
	}
	fclose(pf);
	pf = NULL;
	printf("保存成功。\n");
}

备注:

这里我给了很多链接,是因为理论上来说是用得到这些知识,但是可能会忘记,所以我这里给出链接,需要的可以自行观看一下

相关推荐
凭君语未可几秒前
豆包MarsCode算法题:三数之和问题
java·算法
秋夜白6 分钟前
【排序算法 python实现】
开发语言·python·排序算法
Legendary_00824 分钟前
LDR6020驱动的Type-C接口显示器解决方案
c语言·开发语言·计算机外设
techdashen28 分钟前
Go context.Context
开发语言·后端·golang
凡人的AI工具箱30 分钟前
40分钟学 Go 语言高并发:Select多路复用
开发语言·后端·架构·golang
苏言の狗32 分钟前
CCF认证202406-01 | 矩阵重塑(其一)
c语言·数据结构·c++·算法·矩阵
ModelBulider36 分钟前
SpringMVC应用专栏介绍
java·开发语言·后端·spring·springmvc
恬淡虚无真气从之37 分钟前
go 结构体方法
开发语言·后端·golang
java 乐山38 分钟前
ThinkPad t61p 作SMB服务器,打印服务器,pc ,android ,ipad利用此服务器互传文件
c语言
licy__38 分钟前
Python BeautifulSoup 常用语句详解
开发语言·python·beautifulsoup