初阶数据结构——顺序表以及通讯录的实现
- 顺序表的分类
- 顺序表和数组的区别:顺序表的底层结构是数组,是对数组的封装,实现了常用的增删改查等接口
- 静态顺序表:
//使用宏定义数据类型,方便修改
#define SLDateType int
typedef struct SeqList
{
SLDateType arr[10];//定长数组
int size;//有效数据的个数
}SL;
注意:静态顺序表存在一定的缺陷,空间给少了造成数据丢失,空间给多了造成空间浪费。
- 动态顺序表:
//使用宏定义数据类型,方便修改
#define SLDateType int
typedef struct SeqList
{
SLDateType* arr;//大小可以动态变化
int size;//有效数据的个数
int capacity;//空间容量(即申请空间的总大小)
}SL;
注意:动态顺序表可以进行扩容,一般为原来容量的2~3倍。
- 动态顺序表的实现
为了实现动态顺序表,我们可以通过多文件的方式,让代码的可读性更高。
在这里我们使用两个文件 SL.h 头文件,用于声明顺序表(结构体)以及相关函数,和 SeqList.c 文件,用于实现相关函数。
文件 SL.h :
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#define SLDataType int
typedef struct SeqList
{
SLDataType* arr;//大小可以动态变化
int size;//有效数据的个数
int capacity;//总空间的大小
}SL;
//函数部分
void SLInit(SL* ps);
void SLCheckCapacity(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 SLErease(SL* ps, int pos);
void SLDestroy(SL* ps);
我们可以在头文件中包含头文件,方便后续测试。
注意:使用 SLDataType 来替换 int ,可以让代码适用于各种数据类型,例如整型、浮点型、结构体类型等,方便后期的修改。
文件 SeqList.c 函数讲解:
顺序表的初始化:
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
顺序表的空间检查:
void SLCheckCapacity(SL* ps)
{
int NewCapacity = 0;
if (ps->size == ps->capacity)
{
if (ps->capacity == 0)//这是刚初始化完成,在这里给他赋一个初值,要不然NewCapacity一直是0
{
NewCapacity = 4;
}
else//一般情况下,二倍扩容
{
NewCapacity = 2 * (ps->capacity);
}
//使用函数申请空间
SLDataType* ptr = (SLDataType*)realloc(ps->arr, NewCapacity * sizeof(SLDataType));
assert(ptr != NULL);
ps->arr = ptr;
ps->capacity = NewCapacity;
ptr = NULL;
}
}
当我们想要插入元素时,必须要检查该顺序表的空间是否足够,防止越界。
每执行一次函数功能,仅插入一个元素,因此不用考虑扩容后是否可以容纳全部元素。扩容会涉及到使用函数realloc( ),因此我们定义一个新的变量NewCapacity(包括后续的ptr),防止发生意外来影响我们原先的数据。
当我们第一次给顺序表中添加元素时,由于size和capacity均为0,我们可以给NewCapacity一个处初值4,后续通常扩容原本空间大小的2~4倍。
顺序表的尾插与头插:
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);//检查并且修改空间,之后空间条件满足
ps->arr[ps->size] = x;
(ps->size)++;//记住size的值要发生变化
}
//头插
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps != NULL);
SLCheckCapacity(ps);//检查并且修改空间,之后空间条件满足
//将数据从后向前依次往后移动一位
for (int i = 0; i < ps->size; i++)
{
ps->arr[ps->size - i] = ps->arr[ps->size - 1 - i];
}
ps->arr[0] = x;
(ps->size)++;
}
需要注意size的变化和数据移动的问题,头插过程中,数据是从后向前,依次向后移动一位,这样可以避免数据被覆盖。
顺序表的尾删与头删:
//尾删
void SLPopBack(SL* ps)
{
assert(ps != NULL);
assert(ps->size != 0);//该顺序表至少存在一个元素
(ps->size)--;//直接改变size即可,反正没有赋值的元素是随机值
}
//头删
void SLPopFront(SL* ps)
{
assert(ps != NULL);
assert(ps->size != 0);//该顺序表至少存在一个元素
//将数据从前向后依次向前移动一位
for (int i = 1; i < ps->size; i++)
{
ps->arr[i - 1] = ps->arr[i];
}
(ps->size)--;
}
尾删直接让size减一即可,头删需要让数据从前向后,依次向前移动一位。
指定位置删除与销毁:
//在指定位置插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps != NULL);
assert(pos < ps->size && pos >= 0);
SLCheckCapacity(ps);//检查并且修改空间,之后空间条件满足
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
(ps->size)++;
}
//在指定位置删除数据
void SLErease(SL* ps, int pos)
{
assert(ps != NULL);
assert(pos < ps->size && pos >= 0);
//将数据从前往后依次向前移动一个单位
for (int i = 0; i < (ps->size - pos); i++)
{
ps->arr[pos + i] = ps->arr[pos + i + 1];
}
(ps->size)--;
}
与之前所述的插入与删除相同,通过分析我们可以简单得出数据移动的方向。
顺序表的销毁:
//顺序表的销毁
void SLDestroy(SL* ps)
{
assert(ps != NULL);
if (ps->arr != NULL)
{
free(ps->arr);
ps->arr = NULL;
}
ps->size = ps->capacity = 0;
}
动态内存获取的数据如果不销毁,就会造成内存泄漏。
- 完整版SeqList.c
//顺序表的初始化
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
//顺序表的空间检查——在修改数据之前,应当先检查空间
void SLCheckCapacity(SL* ps)
{
int NewCapacity = 0;
//当元素个数和最大容量相等时,想要插入元素必须扩容
//一次检查,仅仅插入一个元素,不用考虑插入元素的个数,多个元素循环插入
if (ps->size == ps->capacity)
{
if (ps->capacity == 0)//这是刚初始化完成,在这里给他赋一个初值,要不然NewCapacity一直是0
{
NewCapacity = 4;
}
else//一般情况下,二倍扩容
{
NewCapacity = 2 * (ps->capacity);
}
//使用函数申请空间
SLDataType* ptr = (SLDataType*)realloc(ps->arr, NewCapacity * sizeof(SLDataType));
assert(ptr != NULL);
ps->arr = ptr;
ps->capacity = NewCapacity;
ptr = NULL;
}
}
//尾插插入数据
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);//检查并且修改空间,之后空间条件满足
ps->arr[ps->size] = x;
(ps->size)++;//记住size的值要发生变化
}
//头插插入数据
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps != NULL);
SLCheckCapacity(ps);//检查并且修改空间,之后空间条件满足
//将数据从后向前依次往后移动一位
for (int i = 0; i < ps->size; i++)
{
ps->arr[ps->size - i] = ps->arr[ps->size - 1 - i];
}
ps->arr[0] = x;
(ps->size)++;
}
//尾删删除数据
void SLPopBack(SL* ps)
{
assert(ps != NULL);
assert(ps->size != 0);//该顺序表至少存在一个元素
(ps->size)--;//直接改变size即可,反正没有赋值的元素是随机值
}
//头删删除数据
void SLPopFront(SL* ps)
{
assert(ps != NULL);
assert(ps->size != 0);//该顺序表至少存在一个元素
//将数据从前向后依次向前移动一位
for (int i = 1; i < ps->size; i++)
{
ps->arr[i - 1] = ps->arr[i];
}
(ps->size)--;
}
//在指定位置插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps != NULL);
assert(pos < ps->size && pos >= 0);
SLCheckCapacity(ps);//检查并且修改空间,之后空间条件满足
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
(ps->size)++;
}
//在指定位置删除数据
void SLErease(SL* ps, int pos)
{
assert(ps != NULL);
assert(pos < ps->size && pos >= 0);
//将数据从前往后依次向前移动一个单位
for (int i = 0; i < (ps->size - pos); i++)
{
ps->arr[pos + i] = ps->arr[pos + i + 1];
}
(ps->size)--;
}
//顺序表的销毁
void SLDestroy(SL* ps)
{
assert(ps != NULL);
if (ps->arr != NULL)
{
free(ps->arr);
ps->arr = NULL;
}
ps->size = ps->capacity = 0;
}
- 动态顺序表的应用——简易通讯录的实现
通过上述动态顺序表的实现,我们已经获得了两个文件:SL.h 和 SeqList.c 文件,接下来我们就可以使用这两个文件来实现通讯录项目。
- 通讯录基本的功能包括:增加联系人、删除联系人、修改联系人、查找联系人、展示联系人。
由于联系人是一个复杂对象,因此我们需要用到自定义类型——结构体去实现。
再头文件 contact.h 中创造一个联系人结构体,用于储存联系人的信息:
//定义一个联系人的结构体
#define NAME_MAX 20
#define TEL_MAX 20
typedef struct personInfo
{
char name[NAME_MAX];
char tel[TEL_MAX];
}pInfo;
typedef struct Seqlist Contant;//将原本的数据表改为通讯录,更加直观
注意:该处使用代码:typedef struct Seqlist Contant,而不使用:typrdef SL Contant。
- 补充说明:结构体的提前声明
#include<stdio.h>
typedef struct SeqList Contact;
void Fun(Contact* s)
{
printf("struct");
}
struct SeqList
{
int* arr;
int size;
int capacity;
};
int main()
{
Contact* C1 = { 0 };
Fun(C1);
return 0;
}
函数Fun( ) 可以成功执行。
我们在声明结构体前使用typedef给结构体重命名,这就是结构体的提前声明,对应到通讯录中也是一样:
//头文件Contac.h内容
typedef struct personInfo
{
//...
}pInfo;
typedef struct SeqList Contact; //声明部分
void ConInit(Contact* con); //函数声明部分
//头文件SeqList.h头文件
#include"Contact.h"
typedef pInfo SLdt;
typedef struct SeqList //定义部分
{
//...
}SL;
void SLInit(SL* ps);
Contact.h中函数的实现:
我们将这些函数的实现封装在文件 ContactFun.c 文件中。
- 初始化数据和销毁数据
//初始化数据
int R = 0;//仅用于接受scanf函数的返回值
void ConInit(Contact* con)
{
assert(con != NULL);
SLInit(con);
}
//销毁数据
void ConDestory(Contact* con)
{
SLDestory(con);
}
- 添加联系人
注意:以下函数出现的全局变量R,仅用来接受scanf( ) 函数的返回值。
//增加联系人(name+tel)
void ConAdd(Contact* con)
{
pInfo In = { 0 };
//用户手动输入以下信息
printf("请输入你想要添加的联系人的个数:");
int num = 0;
R = scanf("%d", &num);
assert(num >= 1);
for (int i = 1; i <= num; i++)
{
printf("输入第%d个要添加联系人的名字和电话:\n",i);
R = scanf("%s %s", In.name,In.tel);
//用SL中函数添加信息
SLPushBack(con, In);
}
printf("%d个联系人已添加完毕\n",num);
}
我们在之前定义了pInfo结构变量,用于储存联系人的信息。
使用SeqList中的尾插方法来添加联系人。
- 联系人ID的查找:
//联系人的查找
int FindName(Contact* con)
{
char nameF[NAME_MAX] = { 0 };
R = scanf("%s", nameF);
for (int i = 0; i < con->size; i++)
{
if (strcmp(nameF, con->arr[i].name) == 0)
{
return i;
}
}
return -1;
}
该函数用来寻找联系人的ID,和判断是否存在该联系人。使用函数strcmp( ) 函数比较两个字符串是否相等,若存在相等的字符串则返回该联系人在数组中的ID,否则返回-1表示不存在。便于后续函数功能的实现。
- 修改联系人
void ConChange(Contact* con)
{
//输入需要修改的联系人姓名
printf("输入想要修改的联系人姓名:");
int ret = FindName(con);
if (ret == -1)
{
printf("未发现该联系人\n");
}
else//已经发现了,该元素的ID就是ret
{
printf("输入修改后的名字与电话:");
R = scanf("%s %s", con->arr[ret].name, con->arr[ret].tel);
printf("修改成功\n");
return;
}
}
利用了上述联系人ID查找函数FindName( ) ,找到想要修改的联系人后,输入新的名字与电话覆盖即可。
注意:在代码中,Con就表示我们的SeqList结构体(改名后为Contact),通过SeqList的结构体指针找到对应储存联系人信息的数组(即SLdt* arr),找到对应的ID(即arr [ ret ] ),访问得到数组pInfo结构体的元素,即可修改。
- 删除联系人
//删除联系人
void ConDeleat(Contact* con)
{
printf("输入想要删除的联系人姓名:");
int ret = FindName(con);
if (ret == -1)
{
printf("未发现该联系人\n");
}
else//已经发现了,该元素的ID就是ret
{
SLErease(con, ret);
printf("删除成功\n");
return;
}
}
使用SeqList.h文件中的删除元素的函数即可。
- 展示联系人
//展示联系人
void ConShow(Contact* con)
{
for (int i = 0; i < con->size; i++)
{
printf("姓名:%s 电话:%s\n", con->arr[i].name, con->arr[i].tel);
}
printf("已展示全部联系人\n");
}
- 查找联系人
//查找联系人是否存在
void ConFind2(Contact* con)
{
int m = FindName(con);
if (m >= 0)
{
printf("输入你要查找联系人的姓名:\n");
printf("发现该联系人:\n");
printf("姓名:%s 电话:%s\n", con->arr[m].name, con->arr[m].tel);
}
else
{
printf("未发现该联系人\n");
}
}
至此,ContactFun.c 文件中的主要函数就写完了。
下面我们通过main( ) 函数中的程序实现该通讯录:
为了方便实验,我们在SL.h 头文件中编写菜单函数menu( ) :
void menu()
{
printf("****************************\n");
printf("1.增加联系人 2.删除联系人\n");
printf("3.修改联系人 4.查找联系人\n");
printf("5.展示联系人 0.退出通讯录\n");
printf("****************************\n");
}
由于我们在SL.h头文件中包含过Contact.h等文件,在主函数文件中包含一个头文件即可。
#include"SL.h"
int main()
{
int R = 0;//仅用于接受scanf函数的返回值
Contact C = { 0 };
int x = 0;
do
{
menu();
printf("输入你所需要的功能:");
R = scanf("%d", &x);
switch (x)
{
case 1:
ConAdd(&C);
break;
case 2:
ConDeleat(&C);
break;
case 3:
ConChange(&C);
case 4:
ConFind2(&C);
break;
case 5:
ConShow(&C);
break;
case 0:
ConDestory(&C);
break;
default:
printf("输入错误,请重新输入\n");
}
} while (x);
return 0;
}
- 完整代码
- SL.h 头文件:
#pragma once
//Seqlist.h
//可用一个头文件包含多个头文件
#include"Contact.h"
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
typedef pInfo SLdt;
typedef struct SeqList
{
SLdt* arr;
int size;
int capacity;
}SL;
void SLInit(SL* ps);
void SLCheckCapacity(SL* ps);
void SLPushBack(SL* ps, SLdt x);
void SLPushFront(SL* ps, SLdt x);
void SLPopBack(SL* ps);
void SLPopFront(SL* ps);
void SLErease(SL* ps, int pos);
void SLInsert(SL* ps, int pos, SLdt x);
void SLDestory(SL* ps);
- Contact.h 头文件:
#pragma once
//定义一个联系人的结构体
#define NAME_MAX 20
#define TEL_MAX 20
typedef struct personInfo
{
char name[NAME_MAX];
char tel[TEL_MAX];
}pInfo;
typedef struct SeqList Contact;
void ConInit(Contact* con);
void ConAdd(Contact* con);
int FindName(Contact* con);
void ConFind2(Contact* con);
void ConChange(Contact* con);
void ConDeleat(Contact* con);
void ConShow(Contact* con);
void ConDestory(Contact* con);
void menu();
- SeqList.c 文件
#include<assert.h>
#include<stdio.h>
#include<stdlib.h>
#include "SL.h"
//顺序表的初始化
void SLInit(SL* ps)
{
//assert(ps != NULL);
ps->arr = NULL;
ps->capacity = 0;//空间总个数
ps->size = 0;//元素个数
}
//空间的检查与申请
void SLCheckCapacity(SL* ps)
{
int NewCapacity = 0;
if (ps->capacity == ps->size)//相等则表示内存不够了
{
if (ps->capacity == 0)//刚初始化完成的情况
NewCapacity = 4;
else
NewCapacity = (ps->capacity) * 2;//一般情况
SLdt* ptr = (SLdt*)realloc(ps->arr, NewCapacity * sizeof(SLdt));//申请内存
assert(ptr != NULL);
ps->arr = ptr;
ps->capacity = NewCapacity;
ptr = NULL;
}
}
//元素的尾插
void SLPushBack(SL* ps, SLdt x)
{
assert(ps != NULL);
SLCheckCapacity(ps);//检查内存
ps->arr[ps->size] = x;
(ps->size)++;
}
//元素的头插
void SLPushFront(SL* ps, SLdt x)
{
assert(ps != NULL);
SLCheckCapacity(ps);//检查内存
//元素从后向前,依次向后移动一位
for (int i = 0; i < ps->size; i++)
{
ps->arr[ps->size - i] = ps->arr[ps->size - 1 - i];
}
ps->arr[0] = x;
(ps->size)++;
}
//元素的尾删
void SLPopBack(SL* ps)
{
assert(ps != NULL);
(ps->size)--;
}
//元素的头删
void SLPopFront(SL* ps)
{
assert(ps != NULL);
assert(ps->size != 0);
//所有元素从前向后,依次向前移动一位
for (int i = 1; i < ps->size; i++)
{
ps->arr[i - 1] = ps->arr[i];
}
(ps->size)--;
}
//指定位置删除数据
void SLErease(SL* ps, int pos)
{
assert(ps != NULL);
assert(ps->size != 0);
assert(pos >= 0 && pos < ps->size);
//该数据之后的数据,从前往后,依次向前移动一位
for (int i = pos; i < ps->size; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
(ps->size)--;
}
//指定位置插入数据
void SLInsert(SL* ps, int pos, SLdt x)
{
assert(ps != NULL);
assert(pos >= 0 && pos < ps->size);
assert(ps->size != 0);
//该数据之后的数据,从后向前,依次向后移动一位
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
(ps->size)++;
}
//顺序表的销毁
void SLDestory(SL* ps)
{
assert(ps != NULL);
free(ps->arr);
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
- ContactFun.c 文件
#include"SL.h"
//增加联系人、删除联系人、修改联系人、查找联系人、展示联系人
//初始化数据
int R = 0;//仅用于接受scanf函数的返回值
void ConInit(Contact* con)
{
assert(con != NULL);
SLInit(con);
}
//增加联系人(name+tel)
void ConAdd(Contact* con)
{
pInfo In = { 0 };
//用户手动输入以下信息
printf("请输入你想要添加的联系人的个数:");
int num = 0;
R = scanf("%d", &num);
assert(num >= 1);
for (int i = 1; i <= num; i++)
{
printf("输入第%d个要添加联系人的名字和电话:\n",i);
R = scanf("%s %s", In.name,In.tel);
//用SL中函数添加信息
SLPushBack(con, In);
}
printf("%d个联系人已添加完毕\n",num);
}
//联系人的查找
int FindName(Contact* con)//该函数仅用来找ID
{
char nameF[NAME_MAX] = { 0 };
R = scanf("%s", nameF);
for (int i = 0; i < con->size; i++)
{
if (strcmp(nameF, con->arr[i].name) == 0)
{
return i;
}
}
return -1;
}
//修改联系人
void ConChange(Contact* con)
{
//输入需要修改的联系人姓名
printf("输入想要修改的联系人姓名:");
int ret = FindName(con);
if (ret == -1)
{
printf("未发现该联系人\n");
}
else//已经发现了,该元素的ID就是ret
{
printf("输入修改后的名字与电话:");
R = scanf("%s %s", con->arr[ret].name, con->arr[ret].tel);
printf("修改成功\n");
return;
}
}
//删除联系人
void ConDeleat(Contact* con)
{
printf("输入想要删除的联系人姓名:");
int ret = FindName(con);
if (ret == -1)
{
printf("未发现该联系人\n");
}
else//已经发现了,该元素的ID就是ret
{
SLErease(con, ret);
printf("删除成功\n");
return;
}
}
//展示联系人
void ConShow(Contact* con)
{
for (int i = 0; i < con->size; i++)
{
printf("姓名:%s 电话:%s\n", con->arr[i].name, con->arr[i].tel);
}
printf("已展示全部联系人\n");
}
//查找联系人是否存在
void ConFind2(Contact* con)
{
int m = FindName(con);
if (m >= 0)
{
printf("输入你要查找联系人的姓名:\n");
printf("发现该联系人:\n");
printf("姓名:%s 电话:%s\n", con->arr[m].name, con->arr[m].tel);
}
else
{
printf("未发现该联系人\n");
}
}
//销毁数据
void ConDestory(Contact* con)
{
SLDestory(con);
}
//菜单
void menu()
{
printf("****************************\n");
printf("1.增加联系人 2.删除联系人\n");
printf("3.修改联系人 4.查找联系人\n");
printf("5.展示联系人 0.退出通讯录\n");
printf("****************************\n");
}
- 实现文件:
#include"SL.h"
int main()
{
int R = 0;//仅用于接受scanf函数的返回值
Contact C = { 0 };
int x = 0;
do
{
menu();
printf("输入你所需要的功能:");
R = scanf("%d", &x);
switch (x)
{
case 1:
ConAdd(&C);
break;
case 2:
ConDeleat(&C);
break;
case 3:
ConChange(&C);
case 4:
ConFind2(&C);
break;
case 5:
ConShow(&C);
break;
case 0:
ConDestory(&C);
break;
default:
printf("输入错误,请重新输入\n");
}
} while (x);
return 0;
}