家谱树的建立和求解最大路径
家谱树也称为家谱图,用来标识家族中人员的关系等。
给定一个家谱树文件,文件格式是:
1.第一行是一个整型的数n,表示这个家谱树中成员的个数。
2.后面紧跟着n行数据,每行代表一个成员,其格式为:
名字姓性别年龄
由四部分信息组成,其中性别M表示男性,F表示女性。
3.后面继续跟m行数据,m未知,用来表示成员间的父母-孩子关系,其格式为:
孩子名孩子姓父亲名父亲姓母亲名母亲姓
我们要做的工作是:
1.设计合理的数据结构,用来保存每个成员、成员间的关系,以及整个家谱树。
2.根据指定的性别,找到在家谱树中性别都为该性别的最长路径。
3.求解家谱树中年龄之和最大的路径。
比如,我们给定一个家谱树数据文件为:
14
YE YE M 85
NAI NAI F 86
WAI GONG M 87
WAI PO F 88
SHU SHU M 40
GU GU F 45
BA BA M 50
MA MA F 45
JIU JIU M 50
A YI F 55
QI ZI F 30
ZHANG FU M 30
NV ER F 10
ER ZI M 5
SHU SHU YE YE NAI NAI
GU GU YE YE NAI NAI
BA BA YE YE NAI NAI
MA MA WAI GONG WAI PO
JIU JIU WAI GONG WAI PO
A YI WAI GONG WAI PO
ZHANG FU BA BA MA MA
NV ER ZHANG FU QI ZI
ER ZI ZHANG FU QI ZI
其对应的家谱树为:
下面我们按照上面的三个问题分别讨论。
1.成员、成员关系和家谱树的建立
成员有以下几个基本信息:名、姓、性别、年龄。另外,家谱树中成员间的关系又有父亲关系、母亲关系、孩子关系以及兄弟关系。
我们将成员看做一个结构体,其基本信息用具体的数据成员表示。成员间关系用指针来表示,什么关系用什么样的指针指向对方。另外,由于我们需要求解有关性别和年龄的最大路径,所以每个成员中还添加了由来记录最大路径的两个指针。
对于整个家谱树来说其就是元素为成员的一个数组,该数据给定数据文件中指定的成员个数,分配相应的存储空间。
在读取数据文件的过程中完成家谱树的家里,其主要也是分为两部分:成员的建立和成员间关系的建立。成员间的兄弟姐妹关系用next指针来指向对象。
具体相关实现可以参加代码实现和注释说明。
2.求解性别最大路径
给定某种性别(M/F),给定一个起始成员节点,从该节点开始寻找其孩子节点是否也是同种性别,如果不是,则加1返回,如果是,则加1,并且对孩子节点进行递归调用,然后继续访问孩子的兄弟节点,直至将所有孩子都遍历完。
这是一个递归调用的过程。程序中有两个函数实现,但是有一个是错误的。findPathBySex函数对孩子递归调用后,其内部有可能修改了cnt,同时将函数返回值赋予i,之后再比较i和cnt的关系,这时必然i大于或等于cnt,如果大于cnt,则将i赋值给cnt,下一轮兄弟节点的递归调用中,cnt没有退回到从父节点继承过来时的大小,而是包含了前面兄弟节点的累加。这样会造成计数错误。原因就在于形成了cnt到i再到cnt的环,导致cnt一直增加,没有退回到父节点时的大小。比如,在我们给的例子里面,用这个函数执行完后,得到的最大路径长度是5,而得到的具体路径是:YE YE SHU SHU,这是因为到执行完BA BA这个孩子节点后,cnt为4,继续执行SHU SHU节点时,cnt没有退回到1,依然是4,当碰到SHU SHU节点时,cnt有加了1,并将cnt返回给i,这是的i为5,大于之前没有推到的1的cnt(其值为4),所以cnt的值变为了5,而最大路径记录的结果是YE YE的nextBySex指针指向了SHU SHU,而SHU SHU的nextBySex指针为NULL。
我们修改后的函数名为findPathBySex2,这个逻辑结构和findPathBySex差不多,都是对孩子节点进行的递归调用,差别就在于对递归函数参数的处理,以及对递归函数调用返回值的处理。findPathBySex2递归调用后返回到i中,i与之前的记录的最大路径值max比较,如果i大于max,则更新max和nextBySex指针,直至扫描完全部孩子节点。从而得到性别的最大路径和记录下了整个路径。
3.求解年龄最大路径
求解年龄的最大路径和求解性别的最大路径相似,只不过比性别最大路径少了对性别的检测。其递归调用逻辑以及递归函数参数形式与之前的findPathBySex2差不多。
4.测试
完成了以上三部分功能后,剩下的就是测试三部分实现。首先从数据文件中将成员信息和成员关系信息读取出来并建立相应的家谱树。之后就是求解性别最大路径。由于我们求解性别最大路径是针对某个特定节点的,所以我们在求解整个家谱树中的最大路径时,需要遍历整个家谱树,依次检测以每个成员为起始的路径,找到其中的最大路径。有一个改进是在检测前面的成员时,可以设置后面是否已经被访问了,如果已经被访问了,那么就不用再检测了,因为后面即便再检测也不会大于前面的。求解年龄的最大路径和性别最大路径是一样的。当求解完最大路径后,就是根据特定的路径记录指针,将最大路径进行输出。最后是将整个家谱树消毁释放。
5.其他
关于递归调用及其参数的写法
递归调用分为直接递归和间接递归,我们一般情况下用到的是直接递归,在编译原理语法分析的实现中,我们可能会用到间接递归。递归函数的实现要有两个条件:1.递归结束条件;2.递归调用条件。有了这两个条件,我们就能写出可以工作的递归函数。
但是仅仅写出可以工作的递归函数是不够的,递归函数调用完成后,我们还想得到函数调用所产生的结果信息,为了能够得到这些正确的结果信息,我们需要控制递归函数的参数,以及其他一些变量。
首先是递归函数的参数,参数类型可以说是有两种:值传递和引用传递,这两种形式各有各的用途。如果我们想得到整个递归调用函数所有的信息,那么应该用引用传递参数过去,在调用前需要对本参数初始化,如果每次递归调用的值是独立的,彼此没什么关系,那么我们应该用的是值传递形式。
引用传递参数的效果和全局变量以及递归函数内部的的静态局部变量差不多,都是对全局信息的记录。
值传递的效果与递归函数内部一般局部变量的作用差不多,都是针对单次递归调用所需的信息。
引用传递这种需要记录全部递归的结果信息需要进行跨越递归函数间的累加。而值传递只能局限于本次调用,如果值传递造成了环的出现,从而达到了引用传递的效果,那么这种结果必然与初衷相悖,得到的结果也是错误的。递归函数应用值传递参数时要注意是否存在有关值传递的环,如果存在了,那么一个递归调用的值传递参数结果会干扰的其他递归调用的值传递参数。这样是错误的。如果存在环的话,那么需要打破环,具体方法可以是设置一个局部变量,用来记录最大或最小的结果。每次根据值传递参数递归调用完返回的结果,和局部变量进行比较,根据条件进行更新,从而使得递归调用获取正确的信息。
家谱树的改进
在我们这个家谱树中,成员间的关系有父子关系、母子关系、兄弟姐妹关系,但是没有关系,我们可以根据孩子父亲目前这种三元组关系,得到夫妻关系。一般情况下,兄弟姐妹的父母都是一样的,如果存在不同的话,那么还要考虑离异的情况。增加夫妻间关系是家谱树改进的一个方向。
另外,家谱树中的成员时保存在一个动态数组中,我们还可以用链表进行实现,用链表中的节点存储家谱树中的每个成员。
下面我们给出家谱树的实现,具体细节可以查看代码细节和注释说明。
// 家谱树的建立和求解最大路径 #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX 50 + 1 // 宏定义,表示名和姓的最大长度 typedef enum s_{M, F} sex; // 枚举类型,用来表示性别,其中M=0,F=1 // 家谱树中节点的定义 typedef struct node { char name[MAX]; // 名 char surname[MAX]; // 姓 sex sex; // 性别 int age; // 年龄 struct node* next; // 下一个 // 指向兄弟节点,双向关系一般只记录下一个,否则很麻烦,单向关系可以记录全部,也可以记录第一个 struct node* mother; // 母亲 struct node* father; // 父亲 struct node* children; // 孩子 struct node* nextBySex; // 性别指向 struct node* nextByAge; // 年龄指向 } node_t; // 将pt中的指针置为NULL node_t setNULL(node_t pt) // 这里是值传递,函数最后又将形参返回,所以在调用的时候需要给自身赋值 { pt.mother = pt.father = pt.children = pt.next = NULL; pt.nextBySex = pt.nextByAge = NULL; return pt; } // 根据name和surname在rp中找人 node_t* searchPerson(node_t* rp, char* name, char* surname, int n_people) { int i = 0; for (i = 0; i < n_people; ++i) { if (strcmp(rp[i].name, name) == 0 && strcmp(rp[i].surname, surname) == 0) { return &rp[i]; } } return NULL; } // 设置父母关系 node_t* assignParents(node_t* child, node_t* mom, node_t* dad) { child->mother = mom; child->father = dad; return child; } // 设置孩子关系 node_t* assignChild(node_t* parent, node_t* child) { child->next = parent->children; // next指向兄弟 parent->children = child; return parent; } // 读取文件 node_t* readFile(char* filename, int* n_people) { FILE* fp = NULL; char sex = 0, childN[MAX], childS[MAX]; char momN[MAX], momS[MAX], dadN[MAX], dadS[MAX]; int i = 0; node_t* pt = NULL, *current = NULL, *curr_mom = NULL, *curr_dad = NULL; fp = fopen(filename, "r"); if (fp == NULL) { fprintf(stderr, "Error in opening file!"); exit(1); } if (fscanf(fp, "%d", n_people) == EOF) { fprintf(stderr, "Error in reading file!"); exit(1); } pt = (node_t*)malloc((*n_people) * sizeof (node_t)); // 分配一个动态数组,用来存储家谱树中的每个人 if (pt == NULL) { fprintf(stderr, "Error in allocating the memory!"); exit(1); } // 读取后面的n行 while (i < *n_people) { fscanf(fp, "%s %s %c %d", pt[i].name, pt[i].surname, &sex, &pt[i].age); pt[i] = setNULL(pt[i]); if (sex == 'M') { pt[i].sex = M; } else { pt[i].sex = F; } ++i; } // 读取后面的所有行 while (fscanf(fp, "%s %s %s %s %s %s", childN, childS, dadN, dadS, momN, momS) != EOF) { // 找人 current = searchPerson(pt, childN, childS, *n_people); curr_mom = searchPerson(pt, momN, momS, *n_people); curr_dad = searchPerson(pt, dadN, dadS, *n_people); // 设置父母关系 current = assignParents(current, curr_mom, curr_dad); // 设置孩子关系 curr_mom = assignChild(curr_mom, current); curr_dad = assignChild(curr_dad, current); } // 读取完毕,关闭文件 fclose(fp); return pt; } // 寻找同种性别最多的路径 int findPathBySex(node_t* curr, int cnt, sex sex) // cnt这里是传值,所以在后续的调用中,不需要还原 { int i = 0; //int max = 0; // node_t* pt = curr, *tmp = curr->children; // tmp用于暂存children,在后面的处理中children指针被修改 if (pt == NULL || pt->sex != sex) // 如果pt为空,或性别不一致,则终止 { return cnt; } // 如果性别一致,累加cnt ++cnt; // 这个循环有问题,因为在循环中,cnt一直时增加的,会有多余的cnt相加 while (pt->children != NULL) { // 访问孩子节点,递归调用 i = findPathBySex(pt->children, cnt, sex); // 错误:i记录了多余的累加,因为cnt随着递归调用一直累加,而这种累加又由i记录 // 这里形成了环:cnt->i->cnt,这样会造成不断的重复累加,导致计数错误 if (i > cnt) // 说明孩子节点也是sex性别的 { cnt = i; // 这里i可能比cnt大很多,不过相邻的两次递归调用,最多大于1 (*curr).nextBySex = pt->children; } // 下一个孩子,即当前孩子的兄弟 pt->children = pt->children->next; // pt的children指针被修改了,最后需要还原 } pt->children = tmp; return cnt; } // 重写性别最大路径函数 int findPathBySex2(node_t* curr, sex sex, int n) { node_t* pt = NULL, *tmp = NULL; int i = 0, max = 0; pt = curr; tmp = curr->children; if (pt == NULL || pt->sex != sex) { return n; } if (pt->children == NULL) { return n + 1; } while (pt->children != NULL) { i = findPathBySex2(pt->children, sex, n + 1); // 每个递归函数虽然n+1增加了,并返回给i记录,但是没有造成错误的累加记录 // 因为,没有形成环,而之前的实现中形成了环:cnt->i->cnt造成错误累加 // 这里添加了max破坏了i与n之间的环,所以不会造成重复累加 if (i > max) { max = i; (*curr).nextBySex = pt->children; } pt->children = pt->children->next; } curr->children = tmp; return max; } // 寻找年龄最大的路径 int findPathByAge(node_t* curr, int age) // age同findPathBySex中的cnt { int i = 0, max = 0; node_t* pt = curr, *tmp = curr->children; // 同findfindPathByAge if (pt == NULL) // 如果pt为空 { return age; } if (pt->children == NULL) { return age + curr->age; } while (pt->children != NULL) { // 对孩子递归调用 i = findPathByAge(pt->children, age + pt->age); if (i > max) { max = i; (*curr).nextByAge = pt->children; } // 下一个孩子,即孩子的兄弟 pt->children = pt->children->next; } // 还原children指针 pt->children = tmp; return max; // return age; } // 按照nextBySex指针打印同类性别最大的路径 void printBySex(node_t* rp, sex sex) { while (rp != NULL) { printf("%s %s %c ", rp->name, rp->surname, sex == M ? 'M' : 'F'); rp = rp->nextBySex; } printf(" "); } // 按照nextByAge指针打印年龄之和最大的路径 void printByAge(node_t* rp) { while (rp != NULL) { printf("%s %s %d ", rp->name, rp->surname, rp->age); rp = rp->nextByAge; } printf(" "); } int main(int argc, char* argv[]) { node_t* pt = NULL, *pathbySex = NULL, *pathbyAge = NULL; int n_pepole = 0, i = 0, j = 0, max = 0; if (argc != 2) { fprintf(stderr, "Error in passing the arguments! Type <filename> "); exit(1); } pt = readFile(argv[1], &n_pepole); // 读取文件并建立家谱树 // 将nextBySex指针重置 for (i = 0; i < n_pepole; ++i) { pt[i].nextBySex = NULL; } // 寻找M性别最大路径 for (max = 0, i = 0; i < n_pepole; ++i) { if (pt[i].sex != M) { continue; } //j = findPathBySex(&pt[i], 0, M); j = findPathBySex2(&pt[i], M, 0); if (j > max) { max = j; pathbySex = &pt[i]; } } // 打印M性别最大路径 printf("Path by sex:%d males. ", max); printBySex(pathbySex, M); // 将nextBySex指针重置 for (i = 0; i < n_pepole; ++i) { pt[i].nextBySex = NULL; } // 寻找F性别最大路径 for (max = 0, i = 0; i < n_pepole; ++i) { if (pt[i].sex != F) { continue; } //j = findPathBySex(&pt[i], 0, F); j = findPathBySex2(&pt[i], F, 0); if (j > max) { max = j; pathbySex = &pt[i]; } } // 打印F性别最大路径 printf("Path by sex:%d females. ", max); printBySex(pathbySex, F); // 寻找年龄和最大的路径 for (max = 0, i = 0; i < n_pepole; ++i) { j = findPathByAge(&pt[i], 0); if (j > max) { max = j; pathbyAge = &pt[i]; } } // 打印年龄和最大的路径 printf("Path by age:%d years. ", max); printByAge(pathbyAge); // 消毁家谱树 free(pt); return 0; }