一.数据结构
在学习顺序表之前,我们先需要了解什么是数据结构。
1.什么是数据结构呢?
数据结构是由“数据”和结构两词组合而来。
什么是数据呢?
你的游戏账号,身份信息,网页里的信息(文字,视频,图片),手机app存储的信息等等这些都是数据。
什么是结构呢?
在生活中,我们是有很多数据的,当我们想要使用大量的同一类型的数据的时候,通过手动定义大量的独立变量对于程序员的消耗是很大的,并且可读性非常的查,无法轻易的实现查找数据,排列数据等基本功能。可读性非常差,我们可以借助数组这样的数据结构将大量的数据组织在一起,结构也可以理解为组织这样结构的方式。
数据结构是计算机存储,组织数据的方式。数据结构是指相互之间存在一种或者多种特定关系的数据元素的集合。数据结构反应数据内部的构成,即数据由那部分构成,以什么方式构成,以及数据元素之间呈现的结构。
总结:1.能够存储数据(比如顺序表,链表等结构)2.存储的数据方便查找。
2.为什么需要数据结构?
假设在餐馆中,如果我们不使用排队的方式来管理客户点餐,会导致客户就餐感受差,等餐时间长,餐厅营业混乱等情况。同理在程序中,如果不对数据进行管理,可能会导致数据丢失,数据管理困难等问题。所以我们需要通过数据结构,能够将有效数据组织和管理在一起。按照我们的方式任意对数据进行增删改查等操作。
3.最基础的数据结构:数组。
可是数组有一个极大的缺陷,就是在我们声明数组的时候,数组的大小需要固定,可是我们的数据的多少却是无法确定的,数组过大,会导致空间浪费,数组过小,会导致数据丢失。
也就是最基础的数据结构能够提供的操作已经不能完全满足复杂算法的实现了。
二.顺序表
1.顺序表的概念以及结构
1.1线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表,链表,栈,队列,字符串,数组......
线性表在逻辑上是一定是线性结构,也就是连续的一条直线。但是在物理结构上不一定是连续的。
线性表在物理上存储时,通常以数组和链式结构的形式存储。
案例:蔬菜分为绿叶类,瓜类,菌菇类。线性表指的是具有部分相同特性的一类数据结构的集合。
2.顺序表分类
2.1顺序表和数组的区别
顺序表的底层结构式数组,对数组的封装,实现了常用的增删改查等接口
2.2分类
1.静态顺序表
概念:使用定长数组存储元素
typedef int SLDataType;
#define N 7
struct SeqList {
SLDataType arr[N];//定长数组
int size;//存储的有效数据个数
};
typedef struct SeqList SL;
这样的顺序表其实和数组类似:空间少了不够使用,多了会造成空间浪费
2.动态顺序表
struct SeqList {
SLDataType *arr;//按需求申请空间
int size;//存储的有效数据个数
int capacity;//顺序表实际空间容量
};
typedef struct SeqList SL;
什么是按需申请空间呢?
这时候就需要用到我们的malloc函数申请空间用于我们存储数据,如果空间不够了,我们再使用realloc函数对空间进行扩容。
这样的顺序表就非常的方便了
3.顺序表的实现
#pragma once
#define INIT_CAPACITY 4
typedef int SLDataType;
// 动态顺序表 -- 按需申请
typedef struct SeqList
{
SLDataType* a;
int size; // 有效数据个数
int capacity; // 空间容量
}SL;
//初始化和销毁
void SLInit(SL* ps);//初始化
void SLDestroy(SL* ps);//销毁
void SLPrint(SL* ps);//打印数据
//扩容
void SLCheckCapacity(SL* ps);
//头部插⼊删除 / 尾部插⼊删除
void SLPushBack(SL* ps, SLDataType x);//尾插
void SLPopBack(SL* ps);//尾删
void SLPushFront(SL* ps, SLDataType x);//头插
void SLPopFront(SL* ps);//头删
//指定位置之前插⼊/删除数据
void SLInsert(SL* ps, int pos, SLDataType x);//插入
void SLErase(SL* ps, int pos);//删除
int SLFind(SL* ps, SLDataType x);//查找
我们自己实现顺序表的基础就是完成以上所有函数 。
1.首先我们先完成初始化函数。
void SLInit(SL* ps);//初始化
SL ps;
SLInit(&ps);
首先我们来思考一个问题:为什么在初始化函数中我们使用的是SL*类型的指针作为参数?
这是因为如果是使用SL作为参数的话,我们在函数中的任何操作并不会影响实参,形参只是实参的临时拷贝,改变形参并不会影响实参,所以为了改变实参,我们就需要使用一个指向实参的指针。这里我们为了改变这个结构体ps,我们就需要使用一个指向ps的指针。
然后我们再来思考这个初始化函数应该怎么写。
顺序表这个结构体有三个成员变量,对结构体进行初始化,无非就是对这个三个成员变量进行初始化。
顺序表开始是没有存储任何数据的,所以很明显这里的size就是0了.
观察上面的头文件,有着这样一句代码
typedef int SLDataType;
顺序表是可以储存不同种类的数据的,所以我们在使用的时候不能将数据的类型定死了,这是需要根据情况进行改变的,但是我们在实际开发中是不可能对每一个类型一个一个改变的,所以为了方便,我们将我们要存储的类型重命名为SLDataType这样如果我们需要存储其他类型的数据直接进行改变重命名的对象即可。
这也是为什么结构体的指针是SLDatatype*类型的原因了。
我们此时的顺序表是没有储存任何数据的,我们还需要为其开辟空间吗?如果要,那么开辟多大的空间合适呢?
实际上,开不开辟都可以,这个看自己即可,如果初始就开辟会更加方便,这样不用在后续使用的过程中判断空间是否为0了。
第二个问题开辟多大的空间合适呢?
这个是要看具体的实际情况的,这里我们是自己写,不会用到太多的数据,我们就开辟4个元素大小空间即可。
#define INIT_CAPACITY 4
如果觉得太小直接将4进行更改即可。
因为我们要开辟4个空间,所以此时顺序表的容量就是4了。这时候我们就完成了顺序表的初始化了.
void SLInit(SL* ps) {//初始化
ps->a = (SLDataType*)malloc(sizeof(SLDataType) * 4);
ps->size = 0;
ps->capacity = 4;
};
2.完成插入数据函数
接下来就是如何插入元素了
这里解释一下头插尾插什么意思,因为顺序表的本质上是一个数组,只不过这个数组大小是可变的,头插就是在这个顺序表的最前面插入一个数据,尾插就是在顺序表的末尾插入一个数据.头删尾删同理就是删除数据了.
本质上这四种函数其实都是一样的,为了本文的简便性,我们先完成指定位置插入函数和指定位置删除函数,然后再使用这两个函数完成其他函数,也是为了方便大家理解.
void SLInsert(SL* ps, int pos, SLDataType x);
第一个参数就是顺序表结构体的指针了,第二个参数就是指定位置的下表了(position),第三个参数就是我们需要插入的元素.
但是我们需要考虑一个问题,如果顺序表容量不够了怎么办,此时我们是不能进行插入的,否则就造成了数据的丢失,这是非常重大的事故.
所以我们在完成插入函数之前我们需要判断容量是否够 .所以我们可单独再定义一个函数完成此功能.
void SLCheckCapacity(SL* ps) ;
什么时候顺序表的空间不够了呢?
我们要存储的数据大于空间容量就不够了,也就是ps->size > ps->capacity的时候就空间不足了,但是我们存储数据的时候是一个一个进行存储的,也就是说其实两者相等的时候就已经不够了,因为此时我们已经没有办法存储数据了,所以需要realloc函数进行扩容处理.
此时我们又遇到了一个问题:这个空间扩大到多大合适呢?是加1还是加2呢?
如果我们扩容的空间较小,这时候我们就会反复调用这个函数,此时函数压栈会浪费很多性能和时间
程序效率就会低下,所以最好是扩大2倍或者3倍(这个是有数学推导出来的).
void SLCheckCapacity(SL* ps) {//检测空间大小
assert(ps);
if (ps->capacity == ps->size) {
ps->a = realloc(ps->a,sizeof(SLDataType) * ps->capacity * 2);
}
};
此时我们就完成了扩容操作.
但是
这样写对不对呢?
我们知道realloc函数,如果开辟失败是会返回NULL的,如果发生这样的情况,ps->a就变成了NULL,我们不仅开辟失败了,还丢失了访问原来开辟空间的权力,造成了内存泄漏的情况,同时我们也丢失了所有的数据.
所以我们需要定义一个临时变量,来储存realloc函数的返回值,如果返回值不为NULL,我们再把返回值传给ps->a.这样就避免了这样的情况.
以为这样就完了吗?
其实还有一个错误.我们进行了扩容,但是我们并没有改变容量大小,这样也会造成数据丢失的情况,所以容量也要*2.
完整版:
void SLCheckCapacity(SL* ps) {//检测空间大小
assert(ps);
if (ps->capacity == ps->size) {
SLDataType* tmp = realloc(ps->a,sizeof(SLDataType) * ps->capacity * 2);
if (tmp == NULL) {
perror("realloc");
exit(1);
}
ps->a = tmp;
ps->capacity *= 2;
}
};
完成了这些我们再来看插入函数.
假设这里有这样一个顺序表,储存着这样的数据1 2 3 4 5 ... 9
如果我们要在下标为6的位置之前插入一个数据10,也就是在数据6前面插入一个数据10,我们需要怎么完成呢?
首先我们需要对原有的数据进行移动,避免数据丢失了.依次将后面的数据向后移动一位.然后再在下标为5的位置写入数字10,
for (int i = ps->size; i > pos; i--){
ps->a[i] = ps->a[i - 1];
}
ps->a[pos] = x;
我们是从下标为size的位置开始的,所以i从ps->size开始,直到pos的后一位结束.
再移动完了后直接更改即可.
但是这里还有一个错误,也是一个很容易犯错的地方.
我们在插入一个数据后,有效数据的个数其实是变化了的,所以要加1,同理删除要减1,这个一定不能忘了.
完整版:
void SLInsert(SL* ps, int pos, SLDataType x) {//在指定位置之前插入
assert(ps);
assert(pos <= ps->size && pos >= 0);
SLCheckCapacity(ps);
for (int i = ps->size; i > pos; i--){
ps->a[i] = ps->a[i - 1];
}
ps->a[pos] = x;
ps->size++;
};
3.完成删除数据函数
还是上图的顺序表,如果我们要删除下标为5位置上的数据,我们需要将后面的数据向前挪动一位.
void SLErase(SL* ps, int pos) {//删除指定位置的数据
assert(ps);
assert(ps->size > pos && pos >= 0);
while (pos < ps->size) {
ps->a[pos] = ps->a[pos + 1];
pos++;
}
ps->size--;
};
这里之所以要断言是因为要删除的下标必须要小于有效数据的个数,并且大于等于0,不然我们删除的就不是有效数据,这是没有意义的,而且可能会非法访问,出现越界的情况.
不要忘了结束要size--;
4.查找函数
int SLFind(SL* ps, SLDataType x);
x就是我们需要查找的数据,如果找到了函数返回下标,否则返回-1;
我们只需要遍历一遍顺序表即可完成。非常简单。
int SLFind(SL* ps, SLDataType x) {//查找
assert(ps);
for (int i = 0; i < ps->size; i++) {
if (ps->a[i] == x) {
return i;
}
}
return -1;
};
5.头插/删,尾插/删
这个是基于我们前面写的指定位置插入和删除函数写的,这里就不过多赘述。
void SLPushBack(SL* ps, SLDataType x) {
SLInsert(ps,ps->size,x);
};
void SLPopBack(SL* ps) {
SLErase(ps,ps->size-1);
};
void SLPushFront(SL* ps, SLDataType x) {
SLInsert(ps,0,x);
};
void SLPopFront(SL* ps) {
SLErase(ps,0);
};
这样写其实是有好处的,更加便捷,而且不用考虑顺序表是否为空等其他情况。建议读者自行完成这四个函数,不使用指定位置的函数做跳板,可以更加理解顺序表。
6.最后就是销毁顺序表
void SLDestroy(SL* ps) {//销毁顺序表
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
};
ps->a是我们动态开辟的内存,所以最后需要释放掉,并且将指针置为NULL,避免非法访问的情况,最后再将有效数据个数和顺序表容量置为0即可。
4.通讯录
学完了顺序表,我们就可尝试使用顺序表完成一个简单的通讯录了,这个我并没有讲解,因为比较简单,会了顺序表的同学都可以看懂。 以下是代码实现:
//SeqList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include"contact.h"
#define INIT_CAPACITY 4
typedef PeoInfo SLDataType;
// 动态顺序表 -- 按需申请
typedef struct SeqList
{
SLDataType* a;
int size; // 有效数据个数
int capacity; // 空间容量
}SL;
//初始化和销毁
void SLInit(SL* ps);
void SLDestroy(SL* ps);
//void SLPrint(SL* ps);
//扩容
void SLCheckCapacity(SL* ps);
//头部插⼊删除 / 尾部插⼊删除
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
//指定位置之前插⼊/删除数据
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);
int SLFind(SL* ps, SLDataType x);
//SeqList.c
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
//typedef struct SeqList
//{
// SLDataType* a;
// int size; // 有效数据个数
// int capacity; // 空间容量
//}SL;
void SLInit(SL* ps) {//初始化
ps->a = (SLDataType*)malloc(sizeof(SLDataType) * 4);
ps->size = 0;
ps->capacity = 4;
};
void SLDestroy(SL* ps) {//销毁顺序表
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
};
//void SLPrint(SL* ps) {//打印顺序表
// assert(ps);
// int i = 0;
// for (i = 0; i < ps->size; i++) {
// printf("%d->",ps->a[i]);
// }
// printf("\n");
//};
void SLCheckCapacity(SL* ps) {//检测空间大小
assert(ps);
if (ps->capacity == ps->size) {
SLDataType* tmp = realloc(ps->a,sizeof(SLDataType) * ps->capacity * 2);
if (tmp == NULL) {
perror("realloc");
exit(1);
}
ps->a = tmp;
ps->capacity *= 2;
}
};
void SLInsert(SL* ps, int pos, SLDataType x) {//在指定位置之前插入
assert(ps);
assert(pos <= ps->size && pos >= 0);
SLCheckCapacity(ps);
for (int i = ps->size; i>pos; i--){
ps->a[i] = ps->a[i - 1];
}
ps->a[pos] = x;
ps->size++;
};
void SLErase(SL* ps, int pos) {//删除指定位置的数据
assert(ps);
assert(ps->size > pos);
while (pos < ps->size) {
ps->a[pos] = ps->a[pos + 1];
pos++;
}
ps->size--;
};
//int SLFind(SL* ps, SLDataType x) {//查找
// assert(ps);
// for (int i = 0; i < ps->size; i++) {
// if (ps->a->name == x.name) {
// return i;
// }
// }
//};
void SLPushBack(SL* ps, SLDataType x) {
SLInsert(ps,ps->size,x);
};
void SLPopBack(SL* ps) {
SLErase(ps,ps->size-1);
};
void SLPushFront(SL* ps, SLDataType x) {
SLInsert(ps,0,x);
};
void SLPopFront(SL* ps) {
SLErase(ps,0);
};
//contact.h
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#define NAME_MAX 20
#define SEX_MAX 4
#define TEL_MAX 20
#define ADDR_MAX 100
typedef struct SeqList contact;
typedef struct PersonInfo
{
char name[NAME_MAX];
char sex[SEX_MAX];
int age;
char tel[TEL_MAX];
char addr[ADDR_MAX];
}PeoInfo;
//初始化通讯录
void InitContact(contact* con);
//添加通讯录数据
void AddContact(contact* con);
//删除通讯录数据
void DelContact(contact* con);
//展⽰通讯录数据
void ShowContact(contact* con);
//查找通讯录数据
void FindContact(contact* con);
//修改通讯录数据
void ModifyContact(contact* con);
//销毁通讯录数据
void DestroyContact(contact* con);
//contact.c
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
#include<string.h>
//初始化通讯录
//typedef struct PersonInfo
//{
// char name[NAME_MAX];
// char sex[SEX_MAX];
// int age;
// char tel[TEL_MAX];
// char addr[ADDR_MAX];
//}PeoInfo;
void loadContact(contact*con) {
FILE* pf = fopen("contact.txt","rb");
if (pf == NULL) {
perror("fopen");
return;
}
PeoInfo p;
while (fread(&p, sizeof(p), 1, pf)) {
SLPushBack(con,p);
}
};
void InitContact(contact* con) {//初始化通讯录
SLInit(con);
loadContact(con);
};
//添加通讯录数据
void AddContact(contact* con) {
PeoInfo person;
printf("请输入联系人的姓名:\n");
scanf("%s", person.name);
printf("请输入联系人的性别:\n");
scanf("%s", person.sex);
printf("请输入联系人的年龄:\n");
scanf("%d",&(person.age));
printf("请输入联系人的电话:\n");
scanf("%s", person.tel);
printf("请输入联系人的地址:\n");
scanf("%s", person.addr);
SLPushBack(con, person);
printf("添加成功\n");
};
//查找联系人是否存在
int findbyname(contact *con, char* name) {
for (int i = 0; i < con->size; i++) {
if (0 == strcmp(con->a[i].name, name)) {
return i;
}
}
return -1;
}
//删除通讯录数据
void DelContact(contact* con) {
char name[NAME_MAX];
printf("请输入要删除的联系人的名字:\n");
scanf("%s",name);
int pos = findbyname(con, name);
if (pos >= 0) {
SLErase(con,pos);
printf("删除成功!!!\n");
}
else {
printf("要删除的联系人不存在!!!\n");
return;
}
};
//展⽰通讯录数据
void ShowContact(contact* con) {
for (int i = 0; i < con->size; i++) {
printf("%-20s %-4s %-3d %-20s %-100s\n",con->a[i].name,\
con->a[i].sex,\
con->a[i].age,\
con->a[i].tel,\
con->a[i].addr);\
}
};
//查找通讯录数据
void FindContact(contact* con) {
char name[NAME_MAX];
printf("请输入要查找的联系人的名字:\n");
scanf("%s", name);
int pos = findbyname(con, name);
if (pos >= 0) {
printf("查找成功!!!\n");
printf("%-20s %-4s %-3d %-20s %-100s\n", con->a[pos].name, \
con->a[pos].sex, \
con->a[pos].age, \
con->a[pos].tel, \
con->a[pos].addr); \
}
else {
printf("该联系人不存在!!!\n");
}
}
//修改通讯录数据
void ModifyContact(contact* con) {
char name[NAME_MAX];
printf("请输入要修改的联系人的名字:\n");
scanf("%s", name);
int pos = findbyname(con, name);
if (pos >= 0) {
printf("请输入联系人的姓名:\n");
scanf("%s", con->a[pos].name);
printf("请输入联系人的性别:\n");
scanf("%s", con->a[pos].sex);
printf("请输入联系人的年龄:\n");
scanf("%d", &(con->a[pos].age));
printf("请输入联系人的电话:\n");
scanf("%s", con->a[pos].tel);
printf("请输入联系人的地址:\n");
scanf("%s", con->a[pos].addr);
printf("修改成功!\n");
}
else {
printf("该联系人不存在!!!,修改失败!!!\n");
}
};
void SaveContact(contact* con) {
FILE* pf = fopen("contact.txt","wb");
if (pf == NULL) {
perror("fopen\n");
return;
}
//可以一起性读取完
fwrite(con->a, sizeof(PeoInfo), con->size, pf);
//for (int i = 0; i < con->size; i++) {
// fwrite(con->a + i,sizeof(PeoInfo),1,pf);
//}
printf("数据保存成功!!!\n");
}
//销毁通讯录数据
void DestroyContact(contact* con) {
SaveContact(con);
SLDestroy(con);
};
//test.c
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
#include<windows.h>
//void test1() {//测试
// SL ps;
// SLInit(&ps);
// SLPushBack(&ps,1);
// SLPushBack(&ps,2);
// SLPushBack(&ps,3);
//
//
// SLPushFront(&ps,4);
// SLPushFront(&ps,5);
// SLPushFront(&ps, 2);
// SLPrint(&ps);
// int find = SLFind(&ps,2);
// SLErase(&ps,find);
// SLPrint(&ps);
//};
void menu()
{
contact con;
InitContact(&con);
int choice = -1;
do {
printf("---------------------------------------------\n");
printf("--------1.添加联系人 2.删除联系人------------\n");
printf("--------3.查找联系人 4.修改联系人------------\n");
printf("-----------5.展示所有联系人信息--------------\n");
printf("----------------0.退出-----------------------\n");
printf("---------------------------------------------\n");
printf("输入选项:\n");
scanf("%d",&choice);
system("cls");
switch(choice){
case 1:
AddContact(&con);
break;
case 2:
DelContact(&con);
break;
case 3:
FindContact(&con);
break;
case 4:
ModifyContact(&con);
break;
case 5:
ShowContact(&con);
break;
default:
printf("输入有误,请重新输入!!!\n");
break;
};
} while (choice);
DestroyContact(&con);
};
int main()
{
//test1();
menu();
return 0;
}