一、实验目的及要求
1、实验目的
(1)理解家谱管理系统的基本要求和功能。
(2)掌握树这种数据结构的存储结构和基本操作。
(3)学习排序算法并将其应用于家谱成员按出生日期排序的功能。
(4)分析算法的时间复杂度和空间复杂度,掌握算法效率的评估方法。
2、实验要求
家谱管理系统是查询家谱信息必不可少的一部分,利用家谱管理系统可以清楚的了解家族成员信息。本程序实现一个简易的家谱管理系统。
实验要求如下:
(1)该家谱管理系统是通过树来实现,定义其存储结构。
(2)需要设置普通用户、管理员两种角色,不同角色登录后的权限各不相同,普通用户可以进行查询;管理员有对成员增加、删除和修改的权限。
(3)家谱中成员的信息中均应包含姓名、出生日期、婚否、地址、健在否、死亡日期(若其已死亡)等,也可附加其它信息。
(4)查询功能。可按照姓名查询,输出成员信息(包括其本人、父亲、孩子的信息、所在辈分等);按照出生日期查询成员名单。
(5)按出生日期对家谱中所有人排序。
(6)输入任意两个姓名,能够查询这两个姓名之间的是否具有直系祖先后裔关系。
(7)分析算法的时间复杂度和空间复杂度。
二、功能设计
- 登录
- 查询成员
- 增加成员
- 删除成员
- 修改成员信息
- 按出生日期对家谱中所有人排序
- 查询两人是否有父子关系
三、系统流程图
四、数据结构及函数功能模块设计:
1、定义个人信息结构体ElemType,包含姓名、出生日期、是否结婚、住址、是否健在以及若死亡的死亡日期各种信息。
2、定义孩子节点ChildPtr,包含自身的标记即数组下标,还有指针域。
3、定义表头结构CTBox,包含父母的数组下标、个人信息还有指向第一个孩子的指针。
4、定义树结构CTree,包含节点数组、根的位置和节点数还有包含删除后的人数。
5、void User_login(CTree *tree)
参数:CTree *tree为传入包含家谱信息的树。
功能:用户账户密码管理。
6、void main_menu_super(CTree *tree)
参数:CTree *tree为传入包含家谱信息的树。
功能:管理员菜单。
7、void main_menu_normal(CTree *tree)
参数:CTree *tree为传入包含家谱信息的树。
功能:普通用户菜单。
8、char get_choice_menu_super()
功能:管理员菜单的选择。
9、char get_choice_menu_normal()
功能:普通用户菜单的选择。
10、char get_choice_Inquire()
功能:查询系统的选择。
11、char get_choice_Modify()
功能:修改菜单的选择。
12、ElemType Input()
功能:录入成员信息。
13、void init(CTree *tree)
参数:CTree *tree为传入包含家谱信息的树。
功能:初始化家族谱。
14、void show_seniority(CTree *tree, int label)
参数:CTree *tree为传入包含家谱信息的树,label为此人在数组中的位置。
功能:显示查询此人的辈分。
15、Status Exist_Name(CTree *tree)
参数:CTree *tree为传入包含家谱信息的树。
功能:以姓名查询是否存在此人。
16、Status Exist_BothDay(CTree *tree)
参数:CTree *tree为传入包含家谱信息的树。
功能:以出生日期判断是否存在此人。
17、void Inquire(CTree *tree)
参数:CTree *tree为传入包含家谱信息的树。
功能:查询功能。
18、void Add_Child(CTree *tree, int i)
参数:CTree *tree为传入包含家谱信息的树,i为数组中已经存了的人数,接下来就从i继续接着添加。
功能:为数组节点增加孩子。
19、void Add_Parent_Person(CTree *tree, int i)
参数:CTree *tree为传入包含家谱信息的树,i为数组中已经存了的人数,接下来就从i继续接着添加。
功能:增加新成员父亲节点。
20、Status Add_Person(CTree *tree)
参数:CTree *tree为传入包含家谱信息的树。
功能:增加新成员。
21、Status Remove(CTree *tree)
参数:CTree *tree为传入包含家谱信息的树。
功能:删除功能。
22、Status Modify(CTree *tree)
参数:CTree *tree为传入包含家谱信息的树。
功能:修改某人信息。
23、Status Sort_BothDay(CTree *tree)
参数:CTree *tree为传入包含家谱信息的树。
功能:对家谱里的人按照出生日期进行排序。
24、void Destroy(CTree *tree)
参数:CTree *tree为传入包含家谱信息的树。
功能:释放内存空间。
25、void Show_Everyone(CTree *tree)
参数:CTree *tree为传入包含家谱信息的树。
功能:显示家谱表。
五、 核心算法设计与实现
1、采用树来存储,因为一个人可以有多个孩子,所以不用二叉树而用树来存储。存储方式结合双亲表示法和孩子表示法,这样可以方便后续功能的实现。
2、进入程序后首先是登录界面,选择是普通用户还是管理员登录,选择后然后输入账号和密码进入各自的功能界面。账号和密码都是内置的,输入时会有提示。
3、进入功能主菜单后选择相应功能调用相应的函数来实现,如果输入的选项不存在则提示输入错误并重新输入直到输入正确。功能主菜单会一直循环来实现各种功能,直到选择退出登录可以回到登录界面切换账号。
4、主菜单和选择功能放到一个函数中,然后返回选项到另一个函数中,在这个函数中去配对相应的函数来实现功能。
5、求辈分的算法为从这个人一直往根节点遍历,直到遍历到根节点停止,根据遍历的分支数可以求出辈分,也就是这个结点在树的深度。
6、查询功能分为三种,在选择完查询功能后再继续选择查询方式,一种是根据姓名查询,遍历数组,如果存在此名且未被删除则输出这个名字的所有人以及他们的父亲和孩子信息与辈分,否则提示查询不到然后选择是继续查询还是返回主菜单;一种是根据出生日期来查询,如果家谱中存在在这一天出生的则输出在这一天出生的所有人以及他们的父亲和孩子信息与辈分,否则提示查询不到然后选择是继续查询还是返回主菜单;一种是输入两个人的姓名,如果输入的两个姓名有一个不存在就直接提示输入名字不存在然后选择是继续查询还是返回主菜单,否则查询是否有父子关系,然后查询完也选择是继续查询还是返回主菜单。
7、添加新成员首先输入所要添加的人数,然后输入个人信息,然后再为其添加孩子信息,采用头插法往其孩子域添加孩子,与图的邻接表操作方式差不多,然后再添加其父亲是谁,然后为父亲的孩子域再用头插法插入这个孩子。
8、删除成员时可以先选择是否显示家谱信息表,这样可以先了解所要删除的成员,防止误删。删除操作先输入所要删除的人的姓名,如果不存在则提示并退出,否则才继续删除。我最终设计的是删除某成员就将其后代也删除,在现实生活中也如此,如果某人移出家谱其后代一般也跟着移出。删除时我采用的是假删除,用了个全局数组来标记每个人是否被删除,一开始我是想删除结点,但是后来发现这样对其他功能的操作会有影响,对树的其他操作不便。
9、修改信息也是可以先选择是否显示家谱信息表,这样可以先了解所要修改的成员的原先信息,提高修改效率,然后选择是要修改什么信息再继续修改。
10、对家谱成员进行按出生日期排序,由于不能在原数组中进行排序,因为如果排序则原来的下标信息会错乱从而导致树的结构错乱,所以额外增加了个数组作为辅助空间,存储排序后的成员在原先数组中的下标,排序方法类似于选择排序,先选择最小的出生日期放在数组第一个位置,然后将其标记为已排序,然后再接着遍历选出次小的也存入排序数组,如此循环最后排序数组存储的就是排序后的成员下标,最后输出按出生日期排序的所有成员信息,被删除的不输出。
11、对于输入有固定范围的都会有判错机制,比如选择选项时如果输入的选项没有对应的功能即超出范围则会循环重新输入,直到输入正确为止。
六、系统实现
1、程序主界面截图
本次程序总共要13种命令,每种命令通过输入相应的单词就能实现。
2、关键部分的实现过程。
七、结果分析
此家谱管理系统的时间复杂度为O(n2),因为在排序操作时时间复杂度达到了O(n2),即使前面求辈分只需O(logn)以及遍历只需O(n),但平均下来还是O(n2)。
空间复杂度为O(n),需要n个空间来存储n个成员,n个辅助空间来在排序时可以用,总的平均下来为O(n)。
八、实验心得
通过本次课程设计实现了家谱管理系统,存储结构采用的是树,结合双亲表示法和孩子表示法,各种功能的操作也是基于树来实现的。本次课程设计让我学会了关于树的存储方式和相关操作,之前实验都是基于二叉树的没有涉及到树,所以一开始做这个课程设计感到非常陌生,但因此我也学会了许多,对树形结构也有了更深的理解。
本次课程设计的亮点在于采用树来存储而不用二叉树,一个人可以有多个孩子而不止两个。且对于输入都会有判错机制,在输入选项时会判断输入的是否在范围内,如果不在会提示重新输入,直到对为止,在输入删除或编辑哪个成员时也会先判断是否在家谱内,不在会提示并退出。还有在删除或编辑前也可以选择是否先显示家谱表,然后再进行删除或编辑,这样可以先了解,防止删除或编辑出错,然后删除或编辑完可以选择是否继续,实现删除或编辑的连续,不用回到主菜单再重新进入删除或编辑。
目前这个家谱管理系统还存在一定的不足,在输入添加时只能先输入祖先再输入后代,因为在添加成员时需要添加该新成员的父亲是谁,此时会在家谱中搜索,如果搜索不到则会提示找不到,只有已存在了才会将其连接上,如果要解决做这个问题的话就只能不连接,只是将其父亲作为这个新成员结点的一个信息,但这样就无法体现树的应用了,没有实现将祖先和后代连接起来,所以最终我恢复了这个做法,只是输入时会有限制而已。
九、源代码
C语言
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_TREE_SIZE 50
#define LEN 20
#define ERROR 0
#define OK 1
int delete[MAX_TREE_SIZE] = {
0};
typedef int Status;
typedef struct // 个人信息结构体
{
char name[LEN]; // 姓名
char bothDay[LEN]; // 出生日期
int getMatings; // 是否结婚
char address[LEN]; // 其住址
int keepAlive; // 是否健在
char deathDay[LEN]; // 若死亡,死亡日期
} ElemType;
typedef struct CTNode // 孩子节点
{
int child; // 自身的标记,数组下标
struct CTNode *next; // 指针域
} ChildPtr;
typedef struct // 表头结构
{
int parent; // 父母的信息,数组下标
ElemType data; // 个人信息
ChildPtr *firstchild;
} CTBox;
typedef struct // 树结构
{
CTBox nodes[MAX_TREE_SIZE]; // 节点数组
int r, n; // 根的位置和节点数
int denum; // 包含删除后的人数
} CTree;
void User_login(CTree *tree); // 用户账户密码管理
void main_menu_super(CTree *tree); // 管理员菜单
void main_menu_normal(CTree *tree); // 普通用户菜单
char get_choice_menu_super(); // 超级用户菜单的选择
char get_choice_menu_normal(); // 普通用户菜单的选择
char get_choice_Inquire(); // 查询系统的选择
char get_choice_Modify(); // 修改菜单的选择
ElemType Input(); // 录入成员信息函数
void init(CTree *tree); // 初始化家族谱
void show_seniority(CTree *tree, int flag); // 显示查询此人时候的辈分
Status Exist_Name(CTree *tree); // 以姓名查询是否存在此人
Status Exist_BothDay(CTree *tree); // 以出生日期判断是否存在此人
void Inquire(CTree *tree); // 查询功能
void Add_Child(CTree *tree, int i); // 为数组节点增加孩子
void Add_Parent_Person(CTree *tree, int i); // 增加新成员时父亲节点的动态变化
Status Add_Person(CTree *tree); // 增加新成员
Status Remove(CTree *tree); // 删除某人
Status Modify(CTree *tree); // 修改某人信息
Status Sort_BothDay(CTree *tree); // 对家谱里的人按照出生日期进行排序
void Destroy(CTree *tree); // 释放内存空间
void Show_Everyone(CTree *tree); //显示家谱表
void init(CTree *tree)
{
tree->denum = 0;
tree->n = 0;
tree->r = 0;
}
void User_login(CTree *tree)
{
char initial_code[LEN] = {
"123456"}; // 密码
char normalUser[LEN] = {
"normal_account"}; // 普通用户的账号
char superUser[LEN] = {
"super_account"}; // 超级用户的账号
char account[LEN]; // 接收账号输入
char password[LEN]; // 接收密码输入
int login = 1;
while (login)
{
printf("***********\t欢迎进入家族管理系统\t***********\n");
printf("a:普通用户登录\tb:管理员登录\tc:退出程序\n");
printf("*************************************************\n");
int flag = 1, tag = 1;
char ch;
printf("请输入选项选择功能:");
while (flag)
{
fflush(stdin);
scanf("%c", &ch);
fflush(stdin);
if (ch == 'a')
{
printf("请输入普通用户账号(normal_account):");
while (tag)
{
fflush(stdin);
scanf("%s", account);
fflush(stdin);
if (!strcmp(account, normalUser))
{
tag = 0;
}
else
{
printf("账号不存在,请重新输入(normal_account):");
}
}
tag = 1;
printf("请输入普通用户密码(123456):");
while (tag)
{
fflush(stdin);
scanf("%s", password);
fflush(stdin);
if (!strcmp(password, initial_code))
{
printf("普通用户登录成功!\n");
main_menu_normal(tree); // 显示普通用户的菜单
tag = 0;
}
else
{
printf("密码不正确,请重新输入(123456):");
}
}
flag = 0;
}
else if (ch == 'b')
{
printf("请输入管理员账号(super_account):");
while (tag)
{
fflush(stdin);
scanf("%s", account);
fflush(stdin);
if (!strcmp(account, superUser))
{
tag = 0;
}
else
{
printf("账号不存在,请重新输入(super_account):");
}
}
tag = 1;
printf("请输入管理员密码(123456):");
while (tag)
{
fflush(stdin);
scanf("%s", password);
fflush(stdin);
if (!strcmp(password, initial_code))
{
printf("管理员登录成功!\n");
main_menu_super(tree); // 显示管理员的菜单
tag = 0;
}
else
{
printf("密码不正确,请重新输入(123456):");
}
}
flag = 0;
}
else if (ch == 'c')
{
flag = 0;
login = 0;
}
else
{
printf("输入有误,请重新输入:");
}
}
}
}
void main_menu_super(CTree *tree) // 超级用户的菜单
{
int status; // 判断选择后执行的成功与否
int choice; // 根据选择而执行相关功能
int flag = 1;
int tag = 1;
int n, flag1 = 1;
while (flag)
{
choice = get_choice_menu_super();
switch (choice)
{
case 'a':
status = Add_Person(tree); // 调用增加成员函数,如果成功添加,则返回OK,反之,返回ERROR
if (status)
{
printf("添加新成员成功!\n");
}
break;
case 'b':
Inquire(tree); // 调用查询功能
break;
case 'c':
flag1 = 1;
while (flag1)
{
status = Modify(tree); // 调用修改成员函数,如果成功修改,则返回OK,反之,返回ERROR
if (status)
{
printf("修改此人操作成功!\n");
}
else
{
printf("家谱中不存在此人!\n");
}
tag = 1;
while (tag)
{
printf("请输入1或2选择是否继续修改(1:继续 2:退出):");
fflush(stdin);
scanf("%d", &n);
fflush(stdin);
if (n ==