前言

数据结构是计算机存储、组织数据的方式。是指相互之间存在一种或多种特定关系的数据元素合集。数据结构反映数据的内部构成,即数据由哪部分构成,什么方式构成,以及数据元素之间的呈现的结构。

使用数据结构是为了将杂乱无章的数据按特定逻辑关系高效地组织起来,从而在时间(运算速度)与空间(内存占用)上最大化程序的执行与存储效率。

一、线性表(linear list)

n个具有相同特性的数据元素的有限序列。在逻辑上是线性的,在物理结构上不一定是线性的

常见的线性表有:顺序表、链表、栈、队列、字符串…

二、顺序表(Sequential List)

顺序表(Sequential List)是线性表的一种,对数组进行了封装,增加了常用的增删查改等接口

三、静态顺序表和动态顺序表

静态顺序表(Static Sequential List)

1
2
3
4
5
6
7
8
#define SZ 10
struct SSeqList
{
// 静态顺序表,使用的是定长数组
// 少了不够用,多了浪费,不推荐使用
int arr[SZ];
int size; // 有效数据个数
};

动态顺序表(Dynamic Sequential List)

1
2
3
4
5
6
7
8
9
struct DSeqList
{
// 动态数据表,使用动态内存管理函数
// 空间大小可以进行调整
// 推荐使用
int* arr;
int size; // 有效数据个数
int capcity;// 表的空间大小
};

四、实现动态顺序表

4.1 头文件

实现会用到以下头文件

1
2
3
stdio.h // 标准库
string.h // 主要使用其中的动态内存管理函数
assert.h // 使用断言

4.2 动态顺序表的初始化

1
2
3
4
5
6
struct DSeqList
{
int* arr;
int size; // 有效数据个数
int capcity;// 表的空间大小,单位为字节
};

为了方便,这里使用 typedef 关键字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef int SLdata; // 顺序表存放的数据类型,方便修改
struct DSeqList
{
SLdata* arr;
int size;
int capcity;
};

typedef struct DSeqList
{
// 动态数据表,使用动态内存管理函数
// 空间大小可以进行调整
// 推荐使用
SLdata* arr;
int size; // 有效数据个数
int capcity;// 表的空间大小,单位为字节
}SL; // 方便书写

将动态顺序表的初始化封装为一个函数
1
2
3
4
5
6
7
void SLInt(SL* ps)
{
// 使用指针接收,一来是节省空间,而来是在函数内部直接对传来的结构体进行初始化
ps->arr = NULL; // 初始的时候并没有指向
ps->size = 0; // 初始没有有效数据
ps->capcity = 0; // 初始不分配空间
}

测试
1
2
3
4
5
6
7
int main()
{
// 测试动态顺序表的初始化
SL s;
SLInt(&s);
return 0;
}

4.3 动态顺序表的容量检查与扩容

往顺序表中插入数据,需要先行判断顺序表是否有足够的空间进行插入,如果够就可以直接进行值的插入,如果不够就需要扩容空间

判断空间是否足够的依据是有效数据个数 size 是否与空间容量 capcity 相等。

如果相等的话说明满了就需要进行扩容,扩容多少没有定数,但一般选择扩容 1.5 或 2 倍。扩容操作使用 realloc 函数。如果空间为0,直接给予空间一个初始值

将这个过程封装为一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void Capcity_Chek(SL* ps)
{
//这里要增加判断,传进来的不能是个空指针
assert(ps);
// 判断空间是否足够的依据是有效数据个数 size 是否与空间容量 capcity 相等。
if (ps->size == ps->capcity)
{
// 如果相同的话,说明空间已经存满,需要进行扩容
// 首先判断顺序表的空间是否是 0 , 如果是 0 ,给空间一个初始值
// 否则将空间扩容两倍
int NewCapcity = ps->capcity == 0 ? 4 : ps->capcity * 2;
// * sizeof(SLdata) 是为了确保扩容后的空间的字节数满足存放的对应的数据类型个数
SLdata* Newps = (SLdata*)realloc(ps->arr, NewCapcity * sizeof(SLdata));
if (!Newps)
{
// 判断是否扩展成功
perror("realloc");
return ;
}
// 扩展成功让顺序表获得这片空间
ps->arr = Newps;
ps->capcity = NewCapcity;
}
}

4.4 动态顺序表值的插入

4.4.1 尾插

顺序表插入值的一种特殊情况,向当前顺序表的尾部插入一个数值,顺序表的有效数据个数 size 恰好是顺序表的尾部

将尾插的过程封装成一个函数
1
2
3
4
5
6
7
8
9
10
11
void PushBack(SL* ps, SLdata data)
{
// 顺序表尾插
// 进行任何插入操作前都要先检查顺序表的空间容量是否足够
// Capcity_Chek 有判空的逻辑
Capcity_Chek(ps);
// 顺序表的有效数据个数 size 恰好是顺序表的尾部
ps->arr[ps->size++] = data;
// 插入完成后顺序表有效数据个数 size 要自增
//ps->size++;
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
// 测试尾插
SL s;
SLInt(&s);
PushBack(&s, 1);
PushBack(&s, 2);
PushBack(&s, 3);
PushBack(&s, 4);
PushBack(&s, 5);
return 0;
}


4.4.2 头插

顺序表头插是顺序表插值的一种特殊情况,插入一个值到顺序表的首元素位置,插入后整个顺序表的元素要整体向后移动一个 SLdata 大小。

在移动时是从当前顺序表的末尾元素(即下标为 size - 1)的位置开始向后移动,移动后在进行插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void PushFront(SL* ps, SLdata data)
{
// 顺序表头插
// 进行任何插入操作前都要先检查顺序表的空间容量是否足够
// Capcity_Chek 有判空的逻辑
Capcity_Chek(ps);
for (int i = ps->size - 1;i >= 0 ;i--)
{
// 首先将顺序表中原有的所有数据整体向后移动一个 SLdata 大小的空间后在进行插入
ps->arr[i + 1] = ps->arr[i];
}
ps->arr[0] = data;
// 插入完成后顺序表有效数据个数 size 要自增
ps->size++;
}

测试
1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
// 测试头插
SL s;
SLInt(&s);
PushFront(&s, 1);
PushFront(&s, 2);
PushFront(&s, 3);
PushFront(&s, 4);
PushFront(&s, 5);
return 0;
}

4.4.3 指定位置插入

对于顺序表的指定位置 pos 插入,插入到这个位置之前。首先要给指定位置限制一个范围(pos >= 0 且 pos < size)需要先将指定位置及之后的数据整体向后移动一个 SLdata 大小的空间,再将值插入到指定位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void Push_Pos(SL* ps, int pos, SLdata data)
{
// 任意位置插入
// 对于 pos 限制范围, pos >= 0 && pos <= size
// 实际上 pos 为 0 时就是头插, pos 为 size 是就是尾插
if (!(pos >= 0 && pos < ps->size))
{
printf("位置不合法!\n");
return;
}
// 进行任何插入操作前都要先检查顺序表的空间容量是否足够
// Capcity_Chek 有判空的逻辑
Capcity_Chek(ps);

for (int i = ps->size - 1;i >= pos;i--)
{
//将 pos 及之后的数据整体向后移动一个 SLdata 大小的空间
ps->arr[i + 1] = ps->arr[i];
}
// 移动完成后再插入
ps->arr[pos] = data;
// 插入完成后顺序表有效数据个数 size 要自增
ps->size++;
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
// 测试头插
// 测试指定位置前插入数据
SL s;
SLInt(&s);
PushFront(&s, 1);
PushFront(&s, 2);
PushFront(&s, 3);
PushFront(&s, 4);
Push_Pos(&s, 0, 5);// 指定头部插入
Push_Pos(&s, s.size, 6);// 指定尾部插入
Push_Pos(&s, 1, 7); // 指定下标为1的位置插入
Push_Pos(&s, 3, 8); // 指定下标为3的位置插入
return 0;
}


4.5 动态顺序表值的删除

4.5.1 顺序表尾删

顺序表删除的一种的特殊情况,删除的是顺序表尾部的数据,即 size - 1 位置的数据

1
2
3
4
5
6
7
void PopBack(SL* ps)
{
// 顺序表尾删
// 需要判空, 不能传一个空指针, 顺序表中也必须要存在数值才能执行删除操作
assert(ps && ps->size--);
// 直接让 size-- 就行了
}

这里直接让 size– 就可以了,因为 size 表示的是有效数据个数


测试

1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
// 测试尾删
SL s;
SLInt(&s);
PushBack(&s, 1);
PushBack(&s, 2);
PushBack(&s, 3);
PushBack(&s, 4);
PushBack(&s, 5);
PopBack(&s);
return 0;
}

这样顺序表就使用不了被删除的数据

4.5.2 顺序表头删

顺序表删除的一种的特殊情况,删除的是顺序表头部的数据,直接让头元素之后的数据整体向前移动一个 SLdata 大小的空间就行了,同时 size 自减

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void PopFront(SL* ps)
{
// 顺序表头删
// 判空
assert(ps);
// 直接让头元素之后的数据整体向前移动一个 SLdata 大小的空间就行了
for (int i = 0;i < ps->size - 1;i++)
{
// 顺序表中只有1个元素时不会进入循环
ps->arr[i] = ps->arr[i + 1];
}

// 完成删除后 size 要自减
ps->size--;
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main()
{
// 测试头删
SL s;
SLInt(&s);
PushBack(&s, 1);
PushBack(&s, 2);
PushBack(&s, 3);
PushBack(&s, 4);
PopFront(&s);
PopFront(&s);
PopFront(&s);
PopFront(&s);
return 0;
}

4.5.3 顺序表指定位置删除

判断指定位置 pos 的合法性(pos >= 0 且 pos < size),将 pos 后的数据数据整体向前移动一个 SLdata 大小的空间就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void Pop_Pos(SL* ps, int pos)
{
// 指定位置删除
// 判空,不能传一个空指针, 顺序表中也必须要存在数值才能执行删除操作
assert(ps && ps->size);
// 判断指定位置 pos 的合法性(pos >= 0 且 pos < size)
if (!(pos >= 0 && pos < ps->size))
{
printf("位置不合法!\n");
return;
}
// 将 pos 后的数据数据整体向前移动一个 SLdata 大小的空间
for (int i = pos ;i < ps->size - 1 ;i++)
{
// 顺序表中只有1个元素时不会进入循环
ps->arr[i] = ps->arr[i + 1];
}
// 完成删除后 size 要自减
ps->size--;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main()
{
SL s;
SLInt(&s);
PushBack(&s, 1);
PushBack(&s, 2);
PushBack(&s, 3);
PushBack(&s, 4);
PushBack(&s, 5);
PushBack(&s, 6);
Pop_Pos(&s, 0);// 实际上是头删
Pop_Pos(&s, s.size - 1);// 实际上是尾删
Pop_Pos(&s, 2);// 删除下标为2数据
return 0;
}

测试

4.6 动态顺序表值的修改

对指定位置的值进行修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Edit_Pos(SL* ps, int pos,SLdata data)
{
// 指定位置修改数据
// 判空,不能传一个空指针, 顺序表中也必须要存在数值才能执行修改操作
assert(ps && ps->size);
// 判断指定位置 pos 的合法性(pos >= 0 且 pos < size)
if (!(pos >= 0 && pos < ps->size))
{
printf("位置不合法!\n");
return;
}
// 验证位置无误后,进行修改
ps->arr[pos] = data;
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main()
{
// 测试修改
SL s;
SLInt(&s);
PushBack(&s, 1);
PushBack(&s, 2);
PushBack(&s, 3);
PushBack(&s, 4);
Edit_Pos(&s, 0, 5);
Edit_Pos(&s, 1, 5);
Edit_Pos(&s, 2, 5);
Edit_Pos(&s, 3, 5);
return 0;
}

4.7 动态顺序表值的查找

查找指定值是否在表中,查找到了就告知下标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Find(const SL* ps, SLdata data)
{
// 只是查找值,不修改表的值,加上 const 关键字
// 判空,不能传一个空指针, 顺序表中也必须要存在数值才能执行修改操作
assert(ps && ps->size);
// 查找指定值
// 遍历表查找值,找到了就返回下标
for (int i = 0;i < ps->size;i++)
{
if (data == ps->arr[i])
{
printf("找到了,下标是%d !\n",i);
return;
}
}
// 出来说明没找到
printf("当前表中不存在该数据\n");
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
//测试查找
SL s;
SLInt(&s);
PushBack(&s, 1);
PushBack(&s, 2);
PushBack(&s, 3);
PushBack(&s, 4);
Find(&s, 4);
Find(&s, 5);
return 0;
}

4.8 动态顺序表的销毁

动态顺序表是通过动态内存管理实现的,在使用完成后需要销毁
1
2
3
4
5
6
7
8
9
10
11
12
void Destory(SL* ps)
{
// 判空,不能传一个空指针
assert(ps);
// 有效数据个数重置为 0
ps->size = 0;
// 空间容量重置为0
ps->capcity = 0;
// 释放动态申请的空间
free(ps->arr);
ps->arr = NULL;
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
//测试摧毁
SL s;
SLInt(&s);
PushBack(&s, 1);
PushBack(&s, 2);
PushBack(&s, 3);
PushBack(&s, 4);
Destory(&s);
return 0;
}


五、动态顺序表实现简易通讯录

5.1 通讯录结构

使用一个结构体,包含姓名、性别、电话号码

1
2
3
4
5
6
7
8
9
10
11
12
13
#define NAME_MAX 30
#define GENDER_MAX 6
#define TELEPHONE_MAX 20
//重命名方便书写
typedef struct Contact
{
// 通讯录存放的数据的结构
char name[NAME_MAX];
char gender[GENDER_MAX];
char telephone[TELEPHONE_MAX];
}Contact;

typedef struct Contact Con_data;

对应的,顺序表也需要进行修改

1
2
3
4
5
6
typedef struct SeqList
{
Con_data* arr; // 存放的数据类型
int size; // 有效数据个数
int capcity; // 空间容量
}SL;

5.2 通讯录的初始化

1
2
3
4
5
6
7
8
9
void ContactInt(SL* ps)
{
// 通讯录的初始化
// 判空,传进来的不能是个空指针
assert(ps);
ps->arr = NULL;
ps->size = 0;
ps->capcity = 0;
}

测试

1
2
3
4
5
6
int main()
{
SL s;
ContactInt(&s);
return 0;
}

5.3 通讯录空间检查与扩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void CheckCapcity(SL* ps)
{
// 检查通讯录的空间是否足够
// 判空
assert(ps);
//判断依据是 size 和 capcity 是否相等
if (ps->size == ps->capcity)
{
// 没有空间就默认给4个空间,有的话就扩容2倍
int NewCapcity = ps->capcity == 0 ? 4 : ps->capcity * 2;
Con_data* Newp = (Con_data*)realloc(ps->arr, sizeof(Con_data) * NewCapcity);
if (!Newp)
{
// 判断是否开辟成功
perror("realloc");
return;
}
ps->arr = Newp;
ps->capcity = NewCapcity;
}
}

5.4 通讯录数据的插入

采用尾插的方式进行插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void ContactAdd(SL* ps)
{
// 通讯录插入数据
// 判空,传进来的不能是个空指针
assert(ps);
Contact con = { 0 };
printf("请输入要添加的姓名:\n");
printf("> ");
scanf("%s", con.name);

printf("请输入要添加的性别:\n");
printf("> ");
scanf("%s", con.gender);

printf("请输入要添加的电话号码:\n");
printf("> ");
scanf("%s", con.telephone);

//插入数据前需要检查空间是否足够
CheckCapcity(ps);
// 采用尾插的方式插入
ps->arr[ps->size++] = con;
}

测试

1
2
3
4
5
6
7
8
int main()
{
SL s;
ContactInt(&s);
ContactAdd(&s);
ContactAdd(&s);
return 0;
}

5.5 通讯录数据的查找

采用的是根据姓名来查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int ContactFind_ByName(const SL* ps, char* name)
{
// 通讯录查找数据
// 只是查找,不希望会修改所以用 const 关键字
// 判空,传进来的不能是个空指针
assert(ps);
for (int i = 0;i < ps->size;i++)
{
// 字符串比较要用 strcmp
// 相等返回 0
if (!(strcmp(name, ps->arr[i].name)))
{
return i;
}
}
// 出循环说明没找到
printf("当前数据表中不存在该联系人\n");
return 0;
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int ContactFind_ByName(const SL* ps, char* name)
{
// 通讯录查找数据
// 只是查找,不希望会修改所以用 const 关键字
// 判空,传进来的不能是个空指针
assert(ps);
for (int i = 0;i < ps->size;i++)
{
// 字符串比较要用 strcmp
// 相等返回 0
if (!(strcmp(name, ps->arr[i].name)))
{
return i;
}
}
// 出循环说明没找到
printf("当前数据表中不存在该联系人\n");
return -1;
}


5.6 通讯录数据删除

先要通过查找数据才能进行删除,用的是指定位置删除的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void ContactDel_ByName(SL* ps)
{
// 通讯录根据姓名删除数据
// 先要查找才能进行删除
// 查找函数中有判空逻辑
char name[NAME_MAX] = { 0 };
printf("请输入要删除的联系人姓名:\n");
printf("> ");
scanf("%s", name);
int pos = ContactFind_ByName(ps, name);
if (pos == -1)
{
// 说明不存在,直接返回
// 查找函数中存在提示数据不在表中的信息
return;
}
for (int i = pos;i < ps->size - 1;i++)
{
// 姓名对应位置处的后面的数据整体向前面移动一位
ps->arr[i] = ps->arr[i + 1];
}
printf("删除成功!\n");
// 删除后有效数据个数 size 要--
ps->size--;
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
// 测试删除
SL s;
ContactInt(&s);
ContactAdd(&s);
ContactAdd(&s);
ContactAdd(&s);
ContactAdd(&s);
ContactDel_ByName(&s);
return 0;
}


5.7 通讯录数据修改

也是通过姓名匹配到之后进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void Contact_Edit_ByName(SL* ps)
{
// 通讯录根据姓名修改数据
// 先要查找才能进行删除
// 查找函数中有判空逻辑
char name[NAME_MAX] = { 0 };
printf("请输入要修改的联系人姓名:\n");
printf("> ");
scanf("%s", name);
int pos = ContactFind_ByName(ps, name);
if (pos == -1)
{
// 说明不存在,直接返回
// 查找函数中存在提示数据不在表中的信息
return;
}
Contact con = { 0 };
printf("请输入要修改的姓名:\n");
printf("> ");
scanf("%s", con.name);

printf("请输入要修改的性别:\n");
printf("> ");
scanf("%s", con.gender);

printf("请输入要修改的电话号码:\n");
printf("> ");
scanf("%s", con.telephone);

ps->arr[pos] = con;
printf("修改成功!\n");
}

测评

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
// 测试修改数据
SL s;
ContactInt(&s);
ContactAdd(&s);
ContactAdd(&s);
ContactAdd(&s);
Contact_Edit_ByName(&s);
Contact_Edit_ByName(&s);
return 0;
}

5.8 通讯录的销毁

1
2
3
4
5
6
7
8
9
10
11
void Con_Destory(SL* ps)
{
// 通讯录的销毁
// 判空,传进来的不能是个空指针
assert(ps);
ps->size = 0;
ps->capcity = 0;
// 释放动态申请的空间
free(ps->arr);
ps->arr = NULL;
}

六、 通讯录的实现

6.1 简易菜单

1
2
3
4
5
6
7
8
9
10
void menu()
{
printf("1.新增联系人");
printf("2.删除联系人\n");
printf("3.查找联系人");
printf("4.修改联系人\n");
printf("5.所有联系人");
printf("0.退出\n");
printf("选择你的操作:> ");
}

6.2 打印单个联系人的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Print_Person(const SL* ps, int pos)
{
// 判空
assert(ps && ps->size);
if (pos >= 0 && pos < ps->size)
{
printf("姓名 ");
printf("性别 ");
printf("电话 \n");
printf("%3s ", ps->arr[pos].name);
printf("%5s ", ps->arr[pos].gender);
printf("%5s \n", ps->arr[pos].telephone);
}
}

6.3 打印整个通讯录的联系人数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Print_Person(const SL* ps, int pos)
{
// 判空
assert(ps && ps->size);
if (pos >= 0 && pos < ps->size)
{
printf("姓名 ");
printf("性别 ");
printf("电话 \n");
printf("%3s ", ps->arr[pos].name);
printf("%5s ", ps->arr[pos].gender);
printf("%5s \n", ps->arr[pos].telephone);
}
}

6.4 简易通讯录的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
int main()
{
int option = 0;
SL s;
char name[NAME_MAX] = { 0 };

ContactInt(&s);
do
{
menu();
scanf("%d", &option);
switch (option)
{
case 1:
ContactAdd(&s);
break;
case 2:
ContactDel_ByName(&s);
break;
case 3:
printf("请输入要查找的联系人姓名:\n");
scanf("%s", name);
int pos = ContactFind_ByName(&s, name);
if (!pos)
{
printf("找到了!\n");
Print_Person(&s, pos);
}
break;
case 4:
Contact_Edit_ByName(&s);
break;
case 5:
Print_Con(&s);
break;
case 0:
printf("exit!\n");
break;
default:
printf("输入不合法,请重新输入!\n");
}
} while (option);
Con_Destory(&s);
return 0;
}