一、前言
数据结构是什么?
数据结构是计算机存储、组织数据的方式。
数据结构分为数据和结构,所谓数据,那就是教务系统⾥保存的用户信息(姓名、性别、年龄、学历等等)、网页里肉眼可以看到的信息(⽂字、图⽚、视频等等)这些都是数据。
结构则表示数据的类型,各种各样的类型组合到一起,就构成了数据,数据也有了其内部结构。
数据结构能够存储数据、数据能够方便查找。
为什么要有数据结构?
程序中如果不对数据进行管理,可能会导致数据丢失、操作数据困难、野指针等情况。通过数据结构,能够有效将数据组织和管理在⼀起。按照我们的方式任意对数据进行增、删、查、改等操作。
最基础的数据结构是数组。
想要学好数据结构,就先要掌握有关结构体、指针(一级指针、二级指针、指针传参)、结构体指针、动态内存管理相关的知识。
二、顺序表
顺序表的底层结构是数组,通过对数组的封装,实现了常⽤的增删查改等接口。
顺序表的分类
静态顺序表:是一个结构体,里面有两个成员,一个是定长数组,另一个是有效数据长度size。使用定长数组存储元素。缺陷:空间给少了,不够用,给多了嫌浪费。
动态顺序表:是一个结构体,里面有三个成员,一个是数组指针,另一个是有效数据个数size,还有一个是空间容量capacity。
三、动态顺序表的实现
首先创建三个文件,一个头文件 seqlist.h ,函数的声明、类型的声明放在头⽂件(.h)中
一个源文件 seqlist.c 函数的实现是放在源⽂件(.c)中,再创建一个源文件 text.c , 用来对程序进行测试。
首先在seqlist.h中定义一个结构体:
#pragma once
#include<stdio.h>
typedef int SLDataType;//将int命名为SLDataType,方便后续更改类型,只需在这里改就可以。
typedef struct Seqlist
{
SLDataType* arr; //存储数据的底层结构
SLDataType size; //记录顺序表的空间大小
SLDataType capacity;//记录顺序表当前有效的数据个数
}SL;//定义一个结构体并重命名为SL
arr是一个指针变量,它指向一块由realloc函数开辟的一块内存空间,我们的顺序表中的数据就存储在这里。
1.初始化顺序表:
在seqlist.h中定义一个函数:
//初始化
void SLInit(SL* ps);
然后在seqlist.c中实现这两个函数:
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = 0;
ps->capacity = 0;
}//顺序表的初始化
然后在test.c中调试这个函数:
#include"seqlist.h"
void slTest01()
{
SL sl;//创建一个结构体变量,然后调用测试函数01,测试初始化功能是否正常
SLInit(&sl);//将sl的地址传给初始化函数
}
int main()
{
slTest01();//创建一个测试函数01,测试初始化功能是否正常
return 0;
}
经过调试,我们发现数组指针已经置为NULL,其他两个成员也都变成了0。
2.尾部插入数据
我们首先来分析一下尾插:
分三种情况:
第一种:直接尾插
其中capacity = 7,size = 4。
如果把size看做数组的下标,那么直接将数据插入下标为size处就可以
第二种:顺序表中没有元素
其中capacity = 7,size= 0,
第一种情况和第二种情况可看做一种情况。
第三种:顺序表中元素已满
其中capacity = 7,size= 7,这种情况,空间已经不够。需要对数组进行扩容。这里提一下扩容的原则。
一次扩容一个空间,则每次插入数据,就要使用一次动态内存管理函数,使用过于频繁,使程序运行时间变长,效率变低。一次扩容固定空间,那么容易造成空间浪费。而且如果插入数据很多,那么扩容的次数也会变多,程序运行的效率也会变低。这里推荐成倍数地扩容,一般为1.5-2倍。如果扩容倍数为2倍,第一次扩容,开辟两个空间,第二次扩容就开辟四个空间,第三次开辟八个,以此类推,这样程序的效率就会更高。
下面开始尾插的代码实现:
在seqlist.h中定义一个函数:
//尾部插入数据
void SLPushBack(SL* ps, SLDataType x);
然后在seqlist.c中实现这个函数:
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps != NULL);
//空间不够,扩容
//
//空间足够,直接插入
ps->arr[ps->size] = x;
ps->size++;
}//尾部插入数据
以上只实现了前两种情况。
想要实现第三种情况,就要使用动态内存管理函数。
由于头插和尾插都有可能要扩容,那么我们就单独写一个函数,来判断是否需要扩容。
void SLCheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)//意思是有效数据数量等于数组的空间大小,就要扩容。
{
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
//这是一个三目表达式,这句话的意思是,如果ps->capacity == 0为真,那么表达式的值就是4,否则为2 * ps->capacity,然后再将值赋给newCapacity。
//因为capacity的值为0;顺序表刚刚被初始化,里面还没有容量。所以先为顺序表开辟4个空间。
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail");
return 1;
}
else
{
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
}
通过对SLCheckCapacity函数的调用,就可以解决第三种情况。
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps != NULL);
//空间不够,扩容
SLCheckCapacity(ps);
//空间足够,直接插入
ps->arr[ps->size] = x;
ps->size++;
}//尾部插入数据
到这里我们的尾插代码就已经写好了。经过调试,发现结果正确。
在test.c文件中创建了test03函数用于测试尾插代码。结果是capacity成功扩容到了8,观察内存,1-5被全部放在了数组里面。
为了更方便地观察顺序表里面的元素变化,我们写一个函数,使它能够将顺序表里面的元素都打印出来。
3.在屏幕上打印顺序表
在seqlist.h中定义一个函数
//SLPrint函数
void SLPrint(SL* ps);
在seqlist.c 文件中实现这个函数:
void SLPrint(SL* ps)
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
然后在test.c中调用它
void slTest03()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl,1);
SLPushBack(&sl,2);
SLPushBack(&sl,3);
SLPushBack(&sl,4);
SLPushBack(&sl,5);
SLPrint(&sl);//调用这个函数
}
然后运行,就可以看到顺序表的元素被很好地打印在了屏幕上。
4.头部插入数据
接下来我们分析头插。
头插也分为两种情况:
第一种情况:
其中capacity = 7,size= 5,空间足够。
第二种情况:
其中capacity = 7,size = 7,这时想要头部插入数据,就要对顺序表进行扩容。
头插的原理是,先将顺序表中所有元素向后挪动一位,再将数据放入第一个位置。
下面开始代码的实现:
在seqlist.h中定义一个函数:
void SLPushFront(SL* ps, SLDataType x)
然后在seqlist.c中实现这个函数:
void SLPushFront(SL* ps, SLDataType x)
{
int i = 0;
assert(ps != NULL);
//空间不够,扩容
SLCheckCapacity(ps);
//空间足够,直接插入
for (i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];//将顺序表中所有元素向后挪动一位
}
ps->arr[0] = x;//将数据放入第一个位置
ps->size++;
}//头部插入数据
然后在test.c中测试这个函数:
void slTest04()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLPushFront(&sl, 1);
SLPushFront(&sl, 2);
SLPushFront(&sl, 3);
SLPrint(&sl);//直接调用打印顺序表函数,可以直观的观察到顺序表里面的元素
}
运行,结果无误。
5.尾部删除数据
尾部删除数据也分以下几种情况:
第一种,顺序表没有元素,那么不能执行删除操作。
第二种,顺序表中有元素,那样也很简单,直接让size--即可。
由于尾删较为简单,在这里不过多赘述。
下面是代码实现:
void SLPopBack(SL* ps)
{
assert(ps!=NULL);
assert(ps->size != 0);
//顺序表不为空
ps->size--;
}//尾删
6.头部删除数据
与头部插入函数一样,只需让顺序表里面的元素从第二个开始都向前挪动一位即可。
下面是代码实现:
void SLPopFront(SL* ps)
{
assert(ps != NULL);
assert(ps->size != 0);
int i = 0;
for (i = 0; i<ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}//头删
7.任意位置插入数据
想要实现任意位置的插入顺序,那我们需要定义一个函数SLInsert,里面有三个参数,第一个是结构体变量的地址,第二个是想要插入的元素的下标,第三个是想要插入的元素的值。
我们首先定义这个函数:
void SLInsert(SL* ps, int pos, SLDataType x);
然后在seqlist.c中实现:
void SLInsert(SL* ps, int pos, SLDataType x)
{
int i = 0;
assert(ps != NULL);
if (ps->size == 0)
{
//空间不够,扩容
SLCheckCapacity(ps);
//空间足够,直接插入
ps->arr[ps->size] = x;
ps->size++;
}
else
{
//空间不够,扩容
SLCheckCapacity(ps);
for (i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;
}
}//在任意位置插入数据
如果顺序表中元素个数为0,那么就直接插入一个元素。如果顺序表中存在多个元素,那就将下标为pos之后的元素往后挪动一位,再将元素赋给下标为pos的位置处。其中pos为我们想要插入元素的位置的下标。
8.任意位置删除数据
想要删除数据,那么我们就定义一个函数SLErase,它需要两个参数,第一个参数为结构体变量的地址,第二个参数为想要删除的元素。
首先定义函数:
void SLErase(SL* ps, int x)
然后实现函数:
void SLErase(SL* ps, int x)
{
assert(ps!=NULL);
assert(ps->size != 0);
int index = 0;
int i = 0;
while (!(ps->arr[index] == x)&&index<ps->size)
{
index++;
}//通过while循环来找到要删除的元素下标
if (index == ps->size)
{
printf("没有该元素!\n");
return;
}
for (i = index; i < ps->size; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
首先顺序表里面元素个数(size)不能为0,并且传过来的地址不能为空。如果找不到,那就在屏幕上输出错误信息。
9.查找顺序表中的元素
想要查找顺序表中的元素,那我们就定义一个函数,它需要两个参数,一个是结构体变量的地址,另一个是要查找的元素。如果查找到了,那么就返回它的下标,如果找不到,那就返回-1。
下面开始函数实现:
int SLFind(SL* ps, SLDataType x)
{
int index = 0;
while (!(ps->arr[index] == x) && index < ps->size)
{
index++;
}
if (index == ps->size)
{
return -1;
}
else
return index;
}
函数通过while循环来寻找元素的下标。while循环需要满足两个条件,第一个条件是
!(ps->arr[index] == x)
这个表达式需要为真,意思是,没有找到改元素的下标,括号里面的式子为假,为了让循环进行下去,需要在前面加上‘!’号,当找到元素下标并且下标不是最大下标时,整个表达式为假,退出循环,这时index即为该元素的下标或者找不到该元素。
10.顺序表的销毁
当我们使用完毕顺序表之后,由于顺序表的空间是由动态内存管理函数开辟的,我们要使用free函数对它进行销毁。
我们定义一个销毁函数:
void SLDestory(SL* ps);
销毁的原理是,使用free函数释放掉开辟的空间,再将capacity和size置为0,将ps->arr置为NULL。
下面开始实现这个函数:
void SLDestory(SL* ps)
{
free(ps->arr);
ps->arr = NULL;
ps->size = 0;
ps->capacity = 0;
}//顺序表的销毁
至此,我们已经实现了所有顺序表的功能。
四、顺序表的应用:实现一个通讯录
通讯录就是基于顺序表实现的。通讯录应该具有联系人增加、删除、修改的功能。
一个联系人的数据由姓名、电话、家庭住址等组成。我们用一个结构体来存储它们。
下面开始通讯录的代码实现:
首先在创建三个文件:
然后在头文件中将底层写出来:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#define NAME_MAX 100
#define GENDER_MAX 10
#define TEL_MAX 100
#define ADDRESS_MAX 120
//联系人数据的存储
typedef struct PersonalInfo
{
char name[NAME_MAX];
int age;
char gender[GENDER_MAX];
char tel[TEL_MAX];
char addr[ADDRESS_MAX];
}Info;
typedef struct Seqlist
{
Info* arr; //存储数据的底层结构
int size; //记录顺序表的空间大小
int capacity;//记录顺序表当前有效的数据个数
}SL;//定义一个名为SL的结构体
其中arr存储的是一个结构体类型的地址,这个地址指向一块结构体类型的空间,里面存放着联系人数据。
1.打印通讯录菜单
在使用通讯录时,我们希望通讯录能够有一个界面,方便我们更好地使用其中的功能。如下图所示:
通过键盘输入不同的数字,来使用不同的功能。
要使通讯录界面出现在屏幕上,我们需要实现一个打印函数,通过对此函数的调用来实现对
通讯录界面的打印。
打印函数的实现:
void PrintMenu()
{
printf("-------------------------------\n");
printf(" My Address List\n");
printf(" 1.查看已有联系人\n");
printf(" 2.添加新的联系人\n");
printf(" 3.删除已有联系人\n");
printf(" 4.修改联系人信息\n");
printf(" 0.退出通讯录\n");
printf("-------------------------------\n");
printf("请输入您要执行的操作:\n");
}
之后有关函数的定义均在Address_list.h函数中定义,有关函数的实现在Address_list.c文件中实现,笔者就不过多重复了。
实现完了打印函数之后,我们就要对上面的操作进行实现。我们的目的是在键盘上输入相对应的操作,实现相对应的功能。这里就要用到switch语句。我们的要求是输入一个数字,调用相关的函数,然后再次打印通讯录,那我们就可以使用for循环这样来写代码:
int main()
{
PrintMenu();
int n;
int i;
for (i = 0;;)
{
scanf("%d", &n);
switch (n)
{
case 0:
printf("通讯录已退出");
return 0;
case 1:
;
break;
case 2:
;
break;
case 3:
;
break;
case 4:
;
default:
printf("操作无效,请重新输入。\n");
break;
}
PrintMenu();
}
return 0;
}
我们运行一下代码:
由于对应的功能还没有实现,我们先来试一下与界面有关的功能。当我们输入完一个数字之后,然后执行相关的功能,执行完后再将通讯录界面打印出来,再次等待输入数字。当我们输入错误的数字,就会提示错误:
最后当我们要退出通讯录时,输入0即可退出:
至此我们已经完成了有关界面功能的实现。
2.通讯录初始化
与顺序表的初始化一样,直接上代码:
void InfoInit(SL* ps)
{
ps->arr = NULL;
ps->size = 0;
ps->capacity = 0;
}
3.添加联系人数据
定义一个联系人添加函数:
void InfoAdd(SL* ps, Info info);
调用联系人添加函数时,将顺序表的地址和联系人结构体传过去,然后调用scanf函数将联系人的数据输入进去,然后将;联系人的数据再插入到顺序表中:
//添加联系人
void InfoAdd(SL* ps, Info info)
{
assert(ps != NULL);
printf("请输入联系人的姓名:\n");
scanf("%s", info.name);
printf("\n请输入联系人的年龄:\n");
scanf("%d", &info.age);
printf("\n请输入联系人的性别:\n");
scanf("%s", info.gender);
printf("\n请输入联系人的电话:\n");
scanf("%s", info.tel);
printf("\n请输入联系人的住址:\n");
scanf("%s", info.addr);
//空间不够,扩容
int ret = SLCheckCapacity(ps);
//空间足够,直接插入
ps->arr[ps->size] = info;
ps->size++;
}
要注意,info是结构体名但不是结构体的地址,传参时使用的是传值,将空的结构体传给函数,函数接收之后往里面输入数据,将结构体名赋给ps->arr[ps->size],实际上是将整个结构体都赋值过去,编译器会自动开辟一块空间将联系人的数据保存起来,出了函数之后结构体被初始化,再次变成空的结构体然后再次执行上述操作。整个过程中只创建的两个结构体,它们的地址不会改变,只起到搬运数据的作用。我们也可以把创建结构体这一行放在函数里面,变成这个样子:
//添加联系人
void InfoAdd(SL* ps)
{
assert(ps != NULL);
Info info;
printf("请输入联系人的姓名:\n");
scanf("%s", info.name);
printf("\n请输入联系人的年龄:\n");
scanf("%d", &info.age);
printf("\n请输入联系人的性别:\n");
scanf("%s", info.gender);
printf("\n请输入联系人的电话:\n");
scanf("%s", info.tel);
printf("\n请输入联系人的住址:\n");
scanf("%s", info.addr);
//空间不够,扩容
int ret = SLCheckCapacity(ps);
//空间足够,直接插入
ps->arr[ps->size] = info;
ps->size++;
}
即在函数里面创建结构体, 也可以起到相同的作用。
函数里面的扩容函数使用的是顺序表里面的扩容函数。
4.查看联系人数据
要想查看我们添加成功的联系人,非常简单,只需遍历一下顺序表即可。
void InfoView(SL* ps)
{
int i = 0;
printf("姓名 性别 年龄 电话 住址\n");
for (i = 0; i < ps->size; i++)
{
printf("%-8s %-4s %-4d %-11s %-s\n\n",
ps->arr[i].name,
ps->arr[i].gender,
ps->arr[i].age,
ps->arr[i].tel,
ps->arr[i].addr);
}
}
调用该函数可以将联系人的信息打印在屏幕上。
试试效果:
首先来添加联系人:
提示添加完成。再来查看一下:
联系人信息也可以输入中文:
5.删除联系人
当我们想要删除一位联系人,我们可以向屏幕上输入要删除的联系人的名字,然后再执行删除操作。
要想成功地删除联系人,我们的通讯录里面得先有这个联系人的信息。也就是要查找一下。
先来实现一下查找函数:
//删除联系人
int InfoFind(SL* ps, char* name);
如果能够找到这个联系人,那么就返回这个联系人在顺序表当中的下标。如果找不到,就在屏幕上显示出来。
int InfoFind(SL* ps, char* name)
{
int i = 0;
int j = 0;
for (int i = 0; i < ps->size; i++)
{
if (strcmp(ps->arr[i].name, name) == 0)//找到了
{
j = i;
return j;
}
}
if (strcmp(ps->arr[i].name, name) != 0)//如果找到了,就将该联系人的下标返回给j,如果找不到,j就为0,然后退出该程序
{
return -1;
}
return 0;
}
查找的方法是将传过来的字符串与存储的联系人名字一一进行对比,就要使用strcmp函数,如果他的返回值为0,就代表找到了。
接下来实现删除操作:
和顺序表一样,将后面的数据往前挪动一位即可。
void InfoDel(SL* ps, char* name)
{
assert(ps != NULL);
assert(ps->size != 0);
int i = 0;
int j = InfoFind(ps, name);
if (j == -1)
{
printf("该联系人不存在!\n");
return;
}
for (i = j; i < ps->size; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
printf("删除联系人成功!\n\n");
}
调试一下,看看效果:
目前添加的联系人信息:
我们删除一个李四:
可以看到,李四已经被我们成功的删除掉了。
6.修改联系人信息
当我们保存出错的时候,就需要修改联系人的信息。修改也非常简单首先需要查找一下,然后只需将正确的信息重新输入到该联系人的位置上去。
void InfoMod(SL* ps, char* name)
{
assert(ps != NULL);
assert(ps->size != 0);
int j = InfoFind(ps, name);
if (j == -1)
{
printf("该联系人不存在!\n");
return;
}
printf("请输入修改后联系人的名字:\n");
scanf("%s", ps->arr[j].name);
printf("请输入修改后联系人的年龄:\n");
scanf("%d", &ps->arr[j].age);
printf("请输入修改后联系人的性别:\n");
scanf("%s", ps->arr[j].gender);
printf("请输入修改后联系人的电话:\n");
scanf("%s", ps->arr[j].tel);
printf("请输入修改后联系人的住址:\n");
scanf("%s", ps->arr[j].addr);
printf("联系人信息修改成功!\n\n");
}
调试一下:
假如我们修改张三的信息:
我们看到张三的信息也是成功的被修改了。
7.销毁通讯录
当我们不再使用它时,记得将开辟的内存空间释放掉:
void InfoDestory(SL* ps)
{
free(ps->arr);
ps->arr = NULL;
ps->size = 0;
ps->capacity = 0;
}
到这里我们就将通讯录的所有功能都实现了。最后附上源码:
#define _CRT_SECURE_NO_WARNINGS 1
#include"Address_book_2.h"
int main()
{
SL s;//创建一个顺序表结构体变量
InfoInit(&s);
PrintMenu();
int n;
int i;
for (i = 0;;)
{
scanf("%d", &n);
switch (n)
{
case 0:
printf("通讯录已退出");
InfoDestory(&s);//将顺序表结构体变量的地址传过去
return 0;
case 1:
printf("你的联系人信息:\n");
InfoView(&s);
break;
case 2:
printf("执行添加联系人的操作。\n\n");
InfoAdd(&s);
printf("\n联系人添加成功!\n\n");
break;
case 3:
printf("请输入要删除的联系人姓名:\n");
char name[NAME_MAX];
scanf("%s", name);
InfoDel(&s, name);
break;
case 4:
printf("请输入要修改信息的联系人名字:\n");
scanf("%s", name);
InfoMod(&s, name);
break;
default:
printf("操作无效,请重新输入。\n");
break;
}
PrintMenu();
}
return 0;
}
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#define NAME_MAX 100
#define GENDER_MAX 10
#define TEL_MAX 100
#define ADDRESS_MAX 120
//联系人数据的存储
typedef struct PersonalInfo
{
char name[NAME_MAX];
int age;
char gender[GENDER_MAX];
char tel[TEL_MAX];
char addr[ADDRESS_MAX];
}Info;
typedef struct Seqlist
{
Info* arr; //存储数据的底层结构
int size; //记录顺序表的空间大小
int capacity;//记录顺序表当前有效的数据个数
}SL;//定义一个名为SL的结构体
//通讯录界面打印
void PrintMenu();
//通讯录初始化和销毁
void InfoInit(SL* ps);
void InfoDestory(SL* ps);
//添加联系人
void InfoAdd(SL* ps);
//删除联系人
void InfoDel(SL* ps, char* name);
//修改联系人信息
void InfoMod(SL* ps, char* name);
//查看联系人
void InfoView(SL* ps);
//判断是否需要扩容
int SLCheckCapacity(SL* ps);
//查找联系人
int InfoFind(SL* ps, char* name);