【数据结构】顺序表的详细解析及其简单通讯录实现

前言:数据结构是我们学习编程的核心灵魂,前面我们主要只是学习了编程语言的语法,但我们在实际写代码时会发现不知道怎么写,数据结构解决的正是这个问题。数据结构研究的正是数据的组织、管理与存储。下面我将从数据结构中的顺序表开始为大家讲解数据结构及顺序表的简单实践所以可以会有点长


1.顺序表的简单概念介绍

1.1线性表

线性表的定义为:n个有相同特性的数据元素组成的有限序列。而我们今天要学习的顺序表就是属于线性表中一种。线性表在逻辑上连续的一天直线,但在物理结构上不一定是连续的,而我们的顺序表在物理结构上也是连续的,因为顺序表的底层是基于数据来实现的,而数组在内存中的元素是连续存放的,所以我们可以知道顺序表在物理结构上是连续的。

举个例子:在超市买完东西准备结账时,可以看到有很多人排队,但有些队很整齐,有些队不整齐

但他们都需要在前一个人结完账之后才能轮到下一个人,所以在逻辑结构上就是相同的,只是在物理层面上不是整齐的(连续)罢了,这两个队都同样是队列(线性表)


1.2顺序表的分类

前面我们也说过了顺序表的底层为数组,而顺序表只是对这个底层的数据进行了一个封装,提供了一些接口来实现对数组存放的数据进行增删改查的一些接口。

而顺序表又分为两类,第一类为静态顺序表,第二类为动态顺序表。我们知道我们在定义一个数组的时候会定义数组的大小,而顺序表的底层为数组,而静态顺序表正是一开始就把顺序表的大小给它固定了。而动态顺序表顾名思义是在实际的使用过程中,我们根据自身需要为静态顺序表增加了自动扩容的功能:

静态顺序表的定义:

cpp 复制代码
typedef struct Seqlist
{
	SLDDataType arr[100];//定长数组
	int size;//有效数据大小
}SL;

动态顺序表定义:

cpp 复制代码
typedef struct Seqlist
{
	SLDDataType* arr;
	int size;//有效数据大小
	int capacity;//空间大小
}SL;

光靠直觉就可以知道还是动态顺序表更好实现也更加复杂,静态顺序表不太好把握需要的空间的大小,大了会浪费空间,小了又无法满足要求甚至造成数据丢失等严重的问题,所有我下面将主要讲解动态顺序表及具体的实现运用。


2.顺序表实现

前期准备:在写代码之前要把代码根据功能分类为三个子文件:(1)头文件子文件:用于存放头文件及声明函数。(2)实现函数的子文件:用于函数的定义和实现。(3)测试子文件:用于测试功能是否正常。

测试是非常重要的看,因为我之前写过一个小扫雷就没有测试,结果导致出了错误排查大半天,所以最好是写一个功能测试一个功能。这样分类可以防止项目结构混乱,可以提高代码可读性也方便调整修改代码,下面我将逐步带大家来实现顺序表


顺序表的定义及初始化

首先我们将要用到的头文件包含在这个"Swqlist.h"这个文件下,后面我们要调用的函数库的头文件就都包含在这个文件下就好了,其他文件想要引用的话只需要包含一个头文件:

cpp 复制代码
#include "Seqlist.h"

就足够了。

下面是我用到的头文件,可以参考我的,需要什么的话再自己添加就完了:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "ContactBook.h"//用于后面实现通讯录
#include <string.h>

下面是定义顺序表:

cpp 复制代码
typedef struct Seqlist
{
	SLDDataType* arr;//指向存放的数据类型
	int size;//有效空间大小
	int capacity;//空间大小
}SL;

这里我把这个结构体类型重定义为了SL后面就不用这么繁琐了,因为还不知道要存储的数据类型是什么,就先把原来的int也重命名的方便后面的修改。

下面是我们要实现的功能可以现在Seqlist.h文件中都声明了,后面再去Seqlist.c文件中把各个函数逐一实现:

cpp 复制代码
//初始化
void SIlT(SL* ps);
//销毁
void SLDestroy(SL* ps);
//判断空间大小是否满足并申请空间
void SLIsCapaciyaEnough(SL* ps);
//头插
void SLpushFront(SL* ps, SLDDataType x);
//尾插
void SLpushBack(SL* ps, SLDDataType x);
//头删
void SLPopFront(SL* ps);
//尾散
void SLPopBack(SL* ps);
//打印
void SLPrint(SL ps);
//指定位置插入数据
void SLQInsert(SL* ps, int pos, SLDDataType x);
//指定位置删除数据
void SLQErase(SL* ps, int x);
//查找数据
int SLFind(SL* ps, SLDDataType x);

别看好像挺多的,但实际实现下来都挺简单


初始化及销毁:

cpp 复制代码
void SIlT(SL* ps)
{
	ps->arr = NULL;
	ps->capacity = 0;
	ps->size = 0;
}
//销毁函数定义
void SLDestroy(SL* ps)
{
	if (ps->arr)
	{
		free(ps->arr);
	}
	ps->arr = NULL;
	ps->capacity = ps->size = 0;
}

判断空间是否足够并扩容:

cpp 复制代码
void SLIsCapaciyaEnough(SL* ps)
{
	assert(ps);
	if (ps->capacity == ps->size)
	{
		int newCapecity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		SLDDataType* tmp = (SLDDataType*)realloc(ps->arr, newCapecity * sizeof(SLDDataType));
		if (tmp == NULL)
		{
			perror("realloc fail!");
			exit(1);
		}
		ps->arr = tmp;
		ps->capacity = newCapecity;
	}
	
}

后面还有很多个函数要用到这个函数所以我就先把它封装起来了,这个函数的核心逻辑就是当有效空间大小等于顺序表空间大小时,我们还想执行如添加的操作时空间就不够用了,所以我们需要对这个顺序表进行扩容,扩容的大小一般是二倍或者是四倍,这里我就选择了二倍为扩容后的大小,然后在通过realloc函数进行扩容就好了。当然不能传个空指针过来所以我用了assert进行断言。

头插:

cpp 复制代码
void SLpushFront(SL* ps, SLDDataType x)
{
	assert(ps);
	SLIsCapaciyaEnough(ps);
	for (int i = ps->size; i > 0; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[0] = x;
	ps->size++;
}

在顺序表头部插入数据之前要先判断顺序表的空间还够不够,所以我们要先调用SLIsCapaciyaEnough函数进行扩容,在头部插入数据之前要将顺序表的数据整体向后移动一位来保证原来头部的数据不被覆盖掉,所以我这里通过一个for循环来实现了这个目的,最后再将数据输入头部位置就可以了

尾插:

cpp 复制代码
void SLpushBack(SL* ps, SLDDataType x)
{
	assert(ps);
	SLIsCapaciyaEnough(ps);
	ps->arr[ps->size] = x;
	++ps->size;
}

知道了头插的话尾插更简单,在尾部直接插入数据即可当然在插入数据之前也同样要判断空间是否满足并扩容

头删:

cpp 复制代码
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;
}

要删除头部函数先判断空间大小是否为零,在通过for循环将数据整体前移一位,这样我们想删除的数据就会被覆盖掉,达到删除头部数据的目的,当然有效数据也就少了一个,最后再对size--就好了。

尾删:

cpp 复制代码
void SLPopBack(SL* ps)
{
	assert(ps);
	assert(ps->size);
	--ps->size;
}

尾删直接把有效数据size--就好了,有人喜欢先赋值为某个数字(如-1)在把size--但没有必要,直接size--就可以了

指定位置插入数据:

cpp 复制代码
void SLQInsert(SL* ps, int pos, SLDDataType x)
{
	assert(ps->arr);
	assert(pos >= 0 && ps->size >= pos);
	SLIsCapaciyaEnough(ps);
	for (int i = ps->size; i > pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos] = x;
	++ps->size;
}

pos是表示的是数据插入的位置,所以我们不能输入个负数,有效空间的大小也要大于等于pos的大小所以我添加了断言来增加代码的健壮性。再将pos后面的数据整体向后移动一位给要插入的数据留个位置,这里我同样通过for循环来完成这个目的。最后在pos处插入数据,并把size++即可

指定位置删除数据:

cpp 复制代码
void SLQErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && ps->size > pos);
	for (int i = pos; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	--ps->size;
}

这里也是同理直接把pos后的数据整体先前移动一位把原来pos处的数据覆盖掉就可以达成我们删除指定位置数据的目的了

查找数据:

cpp 复制代码
int SLFind(SL* ps, SLDDataType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
		{
			return i;
		}
	}
	return -1;
}

这里我们直接遍历整个顺序表找到该数据再把这个数据的下标位置返回就可以了


3.基于顺序表的通讯录实现

我这里要实现的通讯录是基于顺序表实现的,前面我们写的顺序表就可以作为通讯录的底层,只需要少量代码就可以实现我们的通讯录了

3.1通讯录结构体的定义及其声明

通讯录要存放并修改联系人的信息,所以这里要先定义一个结构体来存放联系人的信息,并将这个结构体类型重命名为了ContactInfo:

cpp 复制代码
typedef struct ContactInfo
{
	char name[NAME_MAX];//名字
	char gender[GENDER_MAX];//性别
	int age;//年龄
	char phone[PHONE_MAX];//电话
	char address[ADDRESS_MAX];//地址
}ContactInfo;

为了方便修改,所以我这里我用#difine来定义常量:

cpp 复制代码
#define NAME_MAX 20
#define GENDER_MAX 10
#define PHONE_MAX 20
#define ADDRESS_MAX 100

前面在Seqlist.h中我不清楚顺序表要存放的数据类型,所以我用了SLDDataType来表示数据类型,当确定要存放的数据类型时,我们就可以通过typedef在Seqlist.h中定义SLDDataType为ContactInfo:

cpp 复制代码
typedef ContactInfo SLDDataType;

并在ContactBook.h中把struct Seqlist重命名为了Contact:

cpp 复制代码
typedef struct Seqlist Contact;

下面是我们要实现的通讯录功能及声明,同样我们还是创建两个文件分别为:

(1)ContactBook.h:用于声明函数及其存放头文件

(2)ContactBook.c:用于实现通讯录

这是通讯录要用到的函数,可以先在ContactBook.h中声明函数:

cpp 复制代码
//通讯录初始化
void ContactInit(Contact* con);
//通讯录销毁
void ContactDestroy(Contact* con);
//通讯录添加数据
void ContactAdd(Contact* con);
//通讯录删除数据
void ContactRemove(Contact* con);
//通讯录的修改
void ContactModify(Contact* con);
//通讯录的查找
void ContactFind(Contact* con);
//展示通讯录所有数据
void ContactShowAll(Contact* con);

3.2通讯录的函数实现

通讯录的初始化及销毁:

cpp 复制代码
//初始化
void ContactInit(Contact* con)
{
	SIlT(con);
}
//销毁
void ContactDestroy(Contact* con)
{
	SLDestroy(con);
}

直接调用我们在顺序表那里写的初始化及销毁函数就可以了

通讯录添加联系人:

cpp 复制代码
void ContactAdd(Contact* con)
{
	ContactInfo info;
	//用户添加数据
	printf("请输入要添加的联系人姓名\n");
	int tmp1 = scanf("%s", info.name);
	printf("请输入要添加的联系人性别\n");
	int tmp2 = scanf("%s", info.gender);
	printf("请输入要添加的联系人年龄\n");
	int tmp3 = scanf("%d", &info.age);
	printf("请输入要添加的联系人电话\n");
	int tmp4 = scanf("%s", info.phone);
	printf("请输入要添加的联系人地址\n");
	int tmp5 = scanf("%s", info.address);
	//在通讯录中输入数据
	SLpushBack(con, info);
}

我们创建了一个联系人结构体数据类型的info用于存放我们要添加的联系人信息,我这里之所以创建很多临时变量来接受scanf的返回值是因为在vs中不接受返回值就会报警告,所以我就加了,实际上也可以不接受返回值,最后再通过之前我们在顺序表里写的尾插函数来存放联系人数据就可以了,当热也可以选择用头插但我这里就使用尾插作为例子了

根据姓名查找函数:

cpp 复制代码
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;
}

我们想要查找某个联系人时,可以自由选择根据什么数据(年龄、性别)来查找满足要求的联系人,我这里就以姓名为查找依据通过strcmp函数来查找联系人,方便后面我们查找或者删除修改联系人,找到了就返回该联系人在顺序表中的位置,找不到就返回一个-1

通讯表删除联系人:

cpp 复制代码
void ContactRemove(Contact* con)
{
	char name[NAME_MAX] = { 0 };
	printf("请输入要删除的联系人姓名\n");
	int rm = scanf("%s", name);
	int find = FindByName(con, name);
	if (find < 0)
	{
		printf("要删除的联系人不存在\n");
		return;
	}
	//存在
	SLQErase(con, find);
	printf("删除成功!\n");
}

当联系人存在时,就可以通过之前我们在顺序表中写的指定位置删除函数来删除这个联系人数据

通讯录的修改:

cpp 复制代码
void ContactModify(Contact* con)
{
	char name[NAME_MAX] = { 0 };
	printf("请输入要修改的联系人姓名\n");
	int rm = scanf("%s", name);
	int find = FindByName(con, name);
	if (find < 0)
	{
		printf("要修改的联系人不存在\n");
		return;
	}
	printf("请输入新的联系人姓名\n");
	int sz1 = scanf("%s", con->arr[find].name);

	printf("请输入新的联系人性别\n");
	int sz2 = scanf("%s", con->arr[find].gender);

	printf("请输入新的联系人年龄\n");
	int sz3 = scanf("%d", &con->arr[find].age);

	printf("请输入新的联系人电话\n");
	int sz4 = scanf("%s", con->arr[find].phone);

	printf("请输入新的联系人地址\n");
	int sz5 = scanf("%s", con->arr[find].address);

	printf("修改成功!\n");
}

我们通过通讯录查找函数找到对应的联系人,对这个联系人的数据进行修改,当然前提是要先判断该联系人是否存在

通讯录的查找:

cpp 复制代码
void ContactFind(Contact* con)
{
	char name[NAME_MAX] = { 0 };
	printf("请输入要查找的联系人姓名\n");
	int rm = scanf("%s", name);
	int find = FindByName(con, name);
	if (find < 0)
	{
		printf("要查找的联系人不存在\n");
		return;
	}
	//存在
	printf("%10s %15s %15s %15s %15s\n", "名字", "性别", "年龄", "电话", "地址");
	printf("%10s %15s %15d %15s %15s\n", con->arr[find].name,
		con->arr[find].gender,
		con->arr[find].age,
		con->arr[find].phone,
		con->arr[find].address
	);
}

这里我们同样通过通讯录查找函数找到对应联系人并把联系人数据通过printf函数打印出来就可以了

展示通讯录的所有联系人:

cpp 复制代码
void ContactShowAll(Contact* con)
{
	//打印表头
	printf("%10s %15s %15s %15s %15s\n", "名字", "性别", "年龄", "电话", "地址");
	for (int i = 0; i < con->size; i++)
	{
		printf("%10s %15s %15d %15s %15s\n", con->arr[i].name,
			                       con->arr[i].gender,
								   con->arr[i].age,
								   con->arr[i].phone,
								   con->arr[i].address
			   );

	}
}

这里通过for来遍历整个顺序表将里面存放的联系人数据分别打印出来就可以了


3.3通讯录展示

写完前面的代码代表着通讯录几乎以及完成了,下面我们在test.c文件中写个简单的接口来简单测试一下:

cpp 复制代码
void ContactBookShowMenu()
{
	printf("=================通讯录=================\n");
	printf("****  1.增加联系人    2.删除联系人  ****\n");
	printf("****  3.修改联系人    4.查找联系人  ****\n");
	printf("****  5.展示联系人    0.  退出      ****\n");
	printf("========================================\n");
}
int main()
{
	int op = -1;
	Contact con;
	ContactInit(&con);
	do {
		ContactBookShowMenu();
		printf("请选择你的操作->");
		int tmp = scanf("%d", &op);
		printf("\n");
		switch (op)
		{
		case 1:
			ContactAdd(&con);
			break;
		case 2:
			ContactRemove(&con);
			break;
		case 3:
			ContactModify(&con);
			break;
		case 4:
			ContactFind(&con);
			break;
		case 5:
			ContactShowAll(&con);
			break;
		case 0:
			printf("退出通讯录\n");
			break;
		default:
			printf("输出错误请重新输入\n");
			break;
		}

	} while (op != 0);
	ContactDestroy(&con);
	return 0;
}

添加联系人:

这里我们看看是否存放进去了:

可以看到数据成功的存放进去,想测试其他功能可以自己去试试,如果想在程序关闭时不丢失数据,可以运用一下我们之前的文件操作将数据保存在文件之中,我就先到这里了


相关推荐
天赐学c语言2 小时前
1.16 - 二叉树的中序遍历 && 动态多态的实现原理
数据结构·c++·算法·leecode
sin_hielo2 小时前
leetcode 2975
数据结构·算法·leetcode
睡一觉就好了。2 小时前
堆的完全二叉树实现
数据结构
多米Domi0112 小时前
0x3f 第33天 redis+链表
数据结构·链表
峥嵘life2 小时前
Android16 EDLA中GMS导入和更新
android·linux·学习
li星野2 小时前
OpenCV4X学习—图像平滑、几何变换
图像处理·学习·计算机视觉
世人万千丶2 小时前
鸿蒙跨端框架 Flutter 学习 Day 3:性能进阶——Iterable 延迟加载与计算流的智慧
学习·flutter·ui·华为·harmonyos·鸿蒙·鸿蒙系统
2401_858286113 小时前
从Redis 8.4.0源码看快速排序(1) 宏函数min和swapcode
c语言·数据库·redis·缓存·快速排序·宏函数
l1t3 小时前
利用豆包辅助编写数独隐式唯一数填充c程序
c语言·开发语言·人工智能·算法·豆包·deepseek