Genealogy Management System
目 录
1. 引言
2. 程序功能展示
3. 各函数间的关系图
4. 程序源程序模块设计说明
模块1:主函数
模块2:菜单类
模块3:主函数功能类
模块4:家谱结点类
模块5:家谱类
5. 总结
附A:程序制作过程记录
附B:程序调试时遇到的问题日志记录
☞ 引言
(一)程序名称:
家谱管理系统(第01版)(DOS界面)(小钱版)
(二)开发环境:
电脑型号: 惠普 HP Pavilion g4 Notebook PC 笔记本电脑
操作系统: Microsoft Windows 7 旗舰版 (6.1.7601 Service Pack 1)
处理器: 英特尔 Core i3 M 390 @ 2.67GHz 双核笔记本处理器
主板: 惠普 1667 (英特尔 HM55 芯片组)
内存: 4 GB ( 尔必达 DDR3 1333MHz / 金士顿 DDR3 1333MHz )
主硬盘: 希捷 ST9640320AS ( 640 GB / 5400 转/分 )
显卡: ATI Radeon HD 6470M ( 1 GB / 惠普 )
编译调试软件:Microsoft Visual Studio2005
(三)主要功能:
01. 录入:录入家庭成员信息。
包括:① 姓名 ② 性别 ③ 出生日期 ④ 死亡日期 ⑤ 血型 ⑥ 职业 ⑦ 备注
02. 修改:修改家谱成员信息。
包括:① 姓名 ② 性别 ③ 出生日期 ④ 死亡日期 ⑤ 血型 ⑥ 职业 ⑦ 备注
03.删除:删除家谱成员信息(删除某成员把其子孙全部删除);
04.查询:查询家谱成员信息
① 显示在家谱中的全部信息。
② 能(搜索)查询指定成员的基本信息。
05.统计:统计并显示结果,统计的项目可以包括
① 所有家谱成员符合条件的平均寿命。
② 所有家谱成员的男女各占人数及其比例。
③ 所有家谱成员的血型各占人数及其比例。
06.家谱信息保存:把家谱中所有的成员信息及关系导出到文件中保存。
07.家谱信息载入:在系统运行时,从已保存的文件中导入家谱成员信息及关系。
08.未实现功能:
① 系统进行操作时,并没考虑家谱中出现家谱成员重名的情况。
② 存放在根结点下的结点的父母名字不会被存放进入结点信息中。
③ 未能自动比较死亡日期是否大于出生日期从而让用户重新进行录入操作。
④ 未能保存多个家谱对象于不同的文件当中。
(四)程序说明:
1. 运行环境:
Windows NT / 2000 / XP / VISTA / 7
2. 文件说明:
① 程序运行文件: 01_GenealogyManagementSystem_可运行程序
② 程序说明书: 02_ GenealogyManagementSystem_说明书.pdf
③ 源程序文件夹: 03_ GenealogyManagementSystem_源程序
④ 程序制作相关: 04_ GenealogyManagementSystem_程序相关
3. 参考资料:
① 《UML和模式应用(原书第三版)》 Craig Larman 著 李洋等 译 机械工业出版社
☞ 程序功能演示
(一)载入:
打开程序后,系统会自动进入“载入家谱数据操作画面”,随后,用户可以选择是否载入上次保存的家谱信息(如下图)
如果选择了“否”,计算机不会读取保存文件,并且出现选择结果提示(如下图)
如果选择了“是”,计算机将读取上次的保存信息,并同时列举读取状态,读取完成时,将会提示用户按下任意键返回主菜单(如下图)
(二)主功能选单:
按下任意键后返回至主菜单(如下图),用户可以根据界面提示,选择出需要操作的数字代号,并按回车键确定,确定后,计算机将提示进入了不同的界面中,并开始与用户互动,并自动运行相对应的操作。
(三)添加操作:
当用户从主菜单中选择“1.为家谱添加新成员”操作后,计算机将转至“添加新成员操作画面”(如图),计算机首先会提示用户输入需要添加的家谱成员数目,待确定用户输入信息后,计算机将对需要输入的家谱成员信息逐项提示,用户只需根据操作提示即可。
当用户将自己希望添加的家谱成员数据全都录入完后,计算机将出现 “添加操作全部完成,请按任意键返回上一层菜单...”的提示(如下图),这时,用户只需按下键盘上任意字母或数字按钮,计算机将自动返回至主菜单。
(三)修改操作:
当用户从主菜单中选择“2. 从家谱中修改成员”操作后,计算机将转至“修改成员操作画面” (如下图),
计算机首先会提示用户选择要通过信息类型检索出正确的修改项,用户正确选择后,计算机将提示用户输入需修改的信息名称,若家谱中不存在该信息,计算机将提示用户选择重新输入还是回到主菜单,若家谱中存在该信息,计算机将列出信息列表并由用户选择需要修改的信息,在用户输入新信息后,计算机将出现 是否继续删除操作的提示,这时,用户只需根据提示键入即可进入到相对应的界面中。
(四)查阅操作:
当用户从主菜单中选择“2. 从家谱中查阅数据”操作后,计算机将转至“查成看成员操作画面” (如下图),
用户此时可根据选单上的信息进行功能选择,当选择了“1. 查看所有家谱成员信息”选项后,计算机将自动将家谱内所有的成员信息列出(如下图)。(计算机在罗列信息的时候,将使用广度遍历树的方法,所以家谱中的成员将以辈份的前后顺序显示。老一辈在前。)
当选择了“2.查看指定家谱成员信息”时,计算机将提示用户要以哪种方式筛选出自己所希望显示的家谱成员,如下图,下图是使用了“名字”类型的筛选方法,
下图是使用了“血型”类型的筛选方法,随后,计算机还会提示是否需要显示该家谱成员是否有儿子,女儿或者其它亲属,用户可根据需要选择。
(五)删除操作:
当用户从主菜单中选择“3. 从家谱中删除成员”操作后,计算机将转至“删除成员操作画面” (如下图),此后,计算机将提示用户输入筛选出家谱成员信息,用户正确入之后,计算机会列出属于该成员下的所有成员信息,然后提醒用户是否继续删除操作,用户可根据需要选择,待选择操作完成后,计算机还会提示用户是否继续删除操作。
(六)统计操作:
当用户从主菜单中选择“5. 统计家谱中的数据”操作后,计算机将转至“统计家谱数据操作画面” (如下图),此后,用户可根据计算机提示选择操作:
当选择了,功能“1. 统计家谱的平均寿命”后,计算机将自动显示出计算结果。(如下图)
当选择了,功能“2. 统计家谱男女各占人数比例”,计算机将自动显示出计算结果。(如下图)
当选择了,功能“3. 统计家谱血型各占人数比例”,计算机将自动显示出计算结果。(如下图)
当选择了,功能“4. 返回到主菜单”(如下图),计算机将自动返回到上一层菜单。
(七)保存操作:
当用户从主菜单中选择“6. 保存家谱中的数据”操作后,计算机将转至“保存家谱数据操作画面” (如下图),此后,计算机将自动进行操作并显示操作结果,此时,用户可根据提示返回到主菜单。
(八)退出操作:
当用户从主菜单中选择“7. 退出家谱管理系统”操作后,计算机将转至“退出家谱系统操作画面” (如下图),同时,计算机将自动进行家谱数据清除操作,用户可根据提示最后退出系统。
☞ 各函数间的关系图
☞ 程序源程序模块设计说明
(一) 模块1:主函数
在这次的主函数编写当中,没有写很长的代码,只是用了一个do-while循环加嵌套一个switch选择语句来实现其功能,主函数中主要控制整个程序结构流程,首先,调用加载功能的函数,然后,显示主菜单让用户选择相对应的功能,并为这些功能提供一个入口。主函数代码结构如下所示:
int main() { genealogy * allInOne = new genealogy; // 创建新对象 Load(allInOne); // 加载历史数据 int choice(0); // 将choice作为循环条件,先对其初始化。 do { choice = allMenu(mainMenu, 1, 7); // 让用户作出选择 std::cin.sync(); switch(choice) { case 1: ... // 功能, 输入功能 case 2: ... // 功能, 修改功能 case 3: ... // 功能, 删除功能 case 4: ... // 功能,查阅功能 case 5: ... // 功能, 统计功能 case 6: ... // 功能, 保存功能 default: ... // 功能,退出选单 }// end switch }while(choice != 7); Exit(allInOne); return 0; }
(二)模块2:菜单类
菜单类主要用于在用户使用各个功能时计算机要输出到屏幕供用户查看,在菜单类中,基本上每一个函数里面都仅使用cout语句,除了一个比较特别的函数,int allMenu(...),其相关信息如下:
操作: int allMenu(void (*menu)(), int min, intmax) 功能:调用调入的形参函数以在屏幕上显示菜单, 另外,实现让用户在min和max数值内输入正确选择数值的功能。 输入:一个void型无参的menu函数,一个整型的最小值,一个整值的最大值。 输出:一个整形的用户在最小值与最大值之间选择的结果。
它的实现方式非常简单,首先清空屏幕,然后,为了避免外部输入流对该次用户输入有影响,所以先调用一次cin.sync()函数,清空输入流,之后,就可以直接调用菜单函数,然后对要求用户输入,由于该次调用可不带最值,不作选单效果,所以可以用一个if语句实现功能,当输入有误时,只需使用while作循环判断,直到用户输入正确为止:
int allMenu(void (*menu)(), int min = 0, int max = 0) { system("cls"); cin.sync(); menu(); int choice(0); if ( !(min == max && min == 0) ) { cin >> choice; while (!cin || choice < min || choice > max) { cout << "\n -> 您的输入有误,请重新输入:"; cin.sync(); cin.clear(); cin >> choice; } } return choice; }
在主函数功能类mainFunction.h中,主要创建了9个子函数以实现各主要功能,或者说,是辅助主函数,引用类的中间函数,在这9个子函数中,除一个intnumToStringFindTye()作用为辅助该类中的函数以保证其功能正确实现外,其它八个均作为“主力”,可以直接用作被主函数调用,它们分别实现了载入、添加、修改、删除、查阅、统计、保存,离开的接口功能。下面是对部分函数的分析。
① 载入函数:void Load(genealogy*& object)
该函数的实现比较简单,只需要清空屏幕,然后对用户显示提示信息,即调用菜单函数,然后直接调用在家谱类中的载入函数即可达到载入信息到内存中供家谱系统使用的效果。
/** * 操作:void Load(genealogy *& object); * 功能:将家谱中的信息从硬盘文件当中读取出来加载到object对象中。 **/ void Load(genealogy *& object) { system("cls"); loadMenu(); object -> load(); cout << "\n-> 操作成功,请按任意键返回主菜单..."; int tempbacktomenu2 = _getch(); return; }
② 实现添加操作的函数:void Input(genealogy*&object)
该函数可以让用户添加用户自定义个函数进入到家谱系统中,可以说,要实现这一功能,由三个步骤组成,第一,让用户输入家谱成员的数目,第二,根据用户的输入,使用循环语句,让用户进行循环输入,直到用户所输入的数目次数,第三,循环语句中要实现的输入到家谱系统中的函数,这里的输入,已经将单个成员的输入操作封装在genealogy类中,所以在编写Input()类的时候,只需要直接调用该函数即可。
/** * 操作:void Input(genealogy *& object) * 功能:在object中添加用户指定个数的家谱成员。 * 前置条件:std::cin, std::cout已经被作用域声明。 * 输入:genealogy类的指针 * 输出: void * 后置条件:genealogy对象被加入了N个用户指定的家谱成员。 **/ void Input(genealogy *& object) { system("cls"); inputMenu(); cout << "-> 现在请输入需要添加的家谱成员数目:"; int num(0); cin >> num; while(!cin.good()) { cin.sync(); cin.clear(); cout << "-> 输入有误,请重新输入:"; cin >> num; } int i(0); cout.fill('0'); // 将填充字段设置为零 while (i != num) { cout << "\n-> 现在开始添加第"; cout.width(3); cout << ++i; cout << " 个家谱成员:\n"; object -> input(); } cout.fill(' '); // 将填充字段还原为空格 cout << "-> 添加操作全部完成,请按任意键返回上一层菜单...\n"; int backtomenu = _getch(); return;}
③ 实现修改操作的函数:void Input(genealogy*& object)
这里的修改家谱成员信息功能实现的主要思路是,先让用户输入关键字类型及关键字,然后通过关键字,从家谱中搜索出相关的成员信息即结点,然后再让用户选择修改成员的信息类型,再让用户输入关键字,从而实现修改功能,而在修改时,在搜出该结点后,可以循环再修改该成员信息,直到该用户对计算机输入不需要修改的信息为止。该函数的算法如下图所示:
下面是代码的实现:
/** * 操作:void Update(genealogy *& object) * 功能:在object中修改用户指定的一个或多个家谱成员。 * 前置条件:std::cin, std::cout已经被作用域声明。有一个非空的家谱。 * 输入:genealogy类的指针 * 输出: void * 后置条件:genealogy对象中的用户指定家谱成员数据被改变。 **/ void Update(genealogy *& object) { if(object -> isempty()) { system("cls"); updateMenu(); cout << ".....” //此处省略 int p = _getch(); return; } bool Updateagain(false); do { system("cls"); updateMenu(); cout << " -> 1. 姓名 2.姓别 3. 出生日期4. 死亡日期 5. 血型 6.工作 7.备注\n" << " -> 请输入用以筛选的家谱信息类型(数字):"; string findtype = intnumToStringFindtype(); cout << " -> 请输入用以筛选的家谱信息关键字,"; string findKey = object -> cinOneNodeType(findtype); // 获得关键字 genealogyNode * findResult = object -> findNode(findtype, findKey, object); if ( NULL == findResult) // 确定寻找的结点如果寻找失败则返回NULL { cout << "-> 很遗憾,家谱中并未发现该信息,请问是否需要再次重复修改信息操作(输入y 或n):"; char choiceloopornot; cin >> choiceloopornot; while (!cin || !( (choiceloopornot != 'y' && choiceloopornot == 'n') || choiceloopornot == 'y' && choiceloopornot != 'n') ) { cin.sync(); cin.clear(); cout << "-> 输入有误,请重新输入:"; cin >> choiceloopornot; } if (choiceloopornot == 'n') { Updateagain = false; break; } else { Updateagain = true; continue; } } else {// else 这里能正确寻得结点,并开始修改结点信息的操作 bool updateFinish(false); do { cout << "\n该结果信息如下:\n"; cout << " 名字 父母名字 姓名 出生日期 死亡日期 血型工作 备注\n"; object -> coutOneNode(findResult); cout << "\n"; cout << " -> 1. 姓名 2.姓别 3. 出生日期4. 死亡日期 5. 血型 6.工作 7.备注\n" << " -> 请输入需要修改的家谱信息类型(数字):"; string findtype = intnumToStringFindtype(); cout << " -> 请输入修改的家谱信息,"; string updateKey = object -> cinOneNodeType(findtype); // 获得关键字 object -> update(findtype, updateKey); // 改变并输出结点的中某信息项 cout << "是否需要继续修改该成员信息:(输入y 或n):"; char choiceloopornot; cin >> choiceloopornot; while (!cin || !( (choiceloopornot != 'y' && choiceloopornot == 'n') || choiceloopornot == 'y' && choiceloopornot != 'n') ) { cin.sync(); cin.clear(); cout << "-> 输入有误,请重新输入:"; cin >> choiceloopornot; } if (choiceloopornot == 'n') { updateFinish = true; } else { updateFinish = false; } }while(updateFinish == false); cout << "\n-> 是否需要再次执行修改家谱成员的操作(输入y 或n):"; char choiceloopornot; cin >> choiceloopornot; while (!cin || !( (choiceloopornot != 'y' && choiceloopornot == 'n') || choiceloopornot == 'y' && choiceloopornot != 'n') ) { cin.sync(); cin.clear(); cout << "-> 输入有误,请重新输入:"; cin >> choiceloopornot; } if (choiceloopornot == 'n') { Updateagain = false; break; } else { Updateagain = true; continue; } } // END ELSE }while(Updateagain); return; }
(四)家谱结点类
家谱结点类,主要用于定义每个家谱成员的数据,此类还可以实现最基本的数据提取存放功能。
※ 数据成员
string name; // 名字
char sex; // 姓别, m代表男姓,f代表女姓
long dieDate; // 死亡日期,[5...8][1...9999][1..12][1...31]
long bornDate; // 出生日期,[5...8][1...9999][1..12][1...31]
char booldType; // 血型,O型:o, AB型:c, A型a, B型b
string job; // 工作
string remark; // 备注,当为空时,用[...]表示
genealogyNode * parent;
genealogyNode * leftChild;
genealogyNode *rightChild;
家谱结点类的数据成员,主要是结点的基本信息,如上所示,包括了姓名,姓别,出生及死亡日期,血型,工作,八项基本资料,当然,还包括了用于N叉树的左右孩子和父母结点。
※ 成员函数
char translateInSex(string); // 将string类型的sex数据转为char型存储
long translateInDate(string); // 将string类型的Date数据转为long型存储
char translateInBooldType(string); // 将string类型的booldType数据转为int型存储
long transInDateHelp(char); // 作为数据转换的辅助函数
stringtransOutDateHelp(long);
stringtranslateOutSex(); //将类中的数据成员sex的数据转出为string类型
stringtranslateOutDate(char type); // 将类中的数据成员Date的数据转出为string类型
stringtranslateOutBooldType(); // 将类中的数据成员booldType数据转出为string类型
由于为了节省磁盘空间,在存放数据成员时,不是全部选择了string型存放,而是将某些数据成员化为long,char类型存放,所以,在输入输出时,则需要一定必要的转换,以方便家谱类进行操作,这里的转换,在输出时,主要还是将非string型的化为string型,在输入时,将string型化为非string型。
① 将日期存储类型由string转换为long的函数:longgenealogyNode::translateInDate(string)
这代码主要有一个有点麻烦的地方,就是如何将字符串,转换为long型时,还可以转化为对应的数值大小,其实很简单,可以从个位数开始提取字符串,然后,将提取出来的数字依次叠加到一位long型变量中,同时,每提取下一位数的时候,将下一位的数值先进一位再加入到数值当中去,进位方法可以直接用数值乘以十的n次方。
/** * 操作:long genealogyNode::translateInDate(string); * 功能:将string类型的Date数据转为long型 * 前置条件:类中的Date的数据为long型 * 输入:一个string类型的数据,数据限定格式为[1...9999][1..12][1...31] * 输出:long型的占四个字节的数据 * 后置条件: 让成员函数赋值给对应的数据成员--Date(bornDate或dieDate) * 输出后的值格式为[5...8][1...9999][1...12][1...31][位数](年)(月)(日) **/ long genealogyNode::translateInDate(string Sdate) { long ldate(0); // 此数据作为最终结果 int stringlong (static_cast<int>(Sdate.length())); // 计算出string的长度 if (stringlong < 5) { exit(1); } else { int i = 0; // 循环条件,也是用来 long powerValue(1); // 次方后的值 while(i != stringlong) { char tempChar = Sdate[i]; ldate += (transInDateHelp(tempChar)* powerValue); powerValue *= 10; ++i; } ldate += i * powerValue; // 在第位上存储数据位数 } return ldate; }
② 将日期存储类型由long转换为string的函数:stringgenealogyNode::translateOutDate(char)
这代码与上一代码的实现方法刚刚相反,同时,它多了一个步骤,判断它要输出的是出生日期还是死亡日期,另外,在实现如何将long型转换为string时,与将类型转换为long型时候的思路逆转即可,即可以先把整个long型的数据提取出来,然后,将它从高位至低位的数字,逐个提取,每次提取后,减掉其数值,即可继续提取下一位的数字,可用取模法也可用减法。
/** * 操作:string genealogyNode::translateOutDate(char); * 功能:将类中long类型的Date数据转为string型返回 * 前置条件:类中的Date的数据为long型 * 输入:char类数值,用于判断是需要转换dieDate还是bornDate,传入'd'或者'b'. * 输出:string型的"XXXX-XX-XX"字符串 * 后置条件: 方便用户在软件中浏览,在计算机中使用输出语句输出。 **/ string genealogyNode::translateOutDate(char type) { string transResult; // 用于保存转换结果的变量. long tempPowerValue(100000000); // 存储在Date内的数据为位数 long tempTotal(0); // 记录需要转换的数值大小 if (type == 'd') { tempTotal = dieDate - (dieDate / tempPowerValue) * tempPowerValue; } else { tempTotal = bornDate - (bornDate / tempPowerValue) * tempPowerValue; } int i(8); for(;i != 0; --i) { if (i == 4 || i == 2) { transResult.append("."); } tempPowerValue /= 10; transResult.append(transOutDateHelp(tempTotal / tempPowerValue)); tempTotal %= tempPowerValue; // 减去最高位的数值的余数 } return transResult; }
(五)家谱类
家谱类,主要用于管理全部的家谱成员,这个家谱使用N叉树的结构存储,使用的操作方法是孩子兄弟表示法,同时,还用到了遍历树的非递归前序遍历及非递归的后序遍历的方法。该家谱类还可以实现基本的添加,删除,修改,查阅等功能。
※ 数据成员
genealogyNode* root; //树根
genealogyNode* fence; //定位梢
家谱类的数据成员很简单,只有两个,第一个是树根,可以方便管理整棵树的结点,第二个是定位梢,用作对家谱的管理操作时使用的结点。
※ 成员函数
bool isempty();
genealogyNode* findParentNode(string, genealogyNode *);
bool genealogy::reverseUserDate(string & );
bool inputDateJudgeHelp(string &);
stringtranslateDateToDateHelp(string);
void input(); // 插入
genealogyNode* genealogy::findNode(string findtype, string key, genealogy *);
void coutOneNode(genealogyNode *);
void coutVectorNode(std::vector<genealogyNode*> & );
void referKeyinformation(string, string, genealogy *,std::vector < genealogyNode *> &);
stringcinOneNodeType(string type);
void update(string, string); // 修改
void referOneNodeDown(genealogyNode * );
void deleteFunction(); // 删除
void removeAll();
void referAll(); // 阅读
void lifeExpectancy(); // 统计功能
void sexRatio();
void booldRatio();
void save();
void load();
这里的各个成员函数之间有些有着一定的依赖关系,具体可以见上面的“各函数关系图”,设计的主要难度在于如何将各个要实现的功能尽量避免重复编写,可以重复调用,尽量将每一个小功能分开函数写的同时又做到不累赘,这个难度有点大,需要在代码编写前充分考虑好各个细节,本程序没有做到这一点,不过,还是有把部分重复功能的函数提取出来了方便多次调用。下面是一些实现方法比较复杂的函数的实现方法,其中包括模拟递归的前序遍历,后序遍历,如何判断插入新结点,如何判断日期的输入是否正确:
① 使用了模拟递归前序遍历的方法找相关结点的函数:genealogyNode *genealogy::findParentNode(...)
前序遍历的模拟,实现起来非常方便,只需要用一个循环语句,一个判断句,再加一个栈变量就可以实现了,将基准条件设为栈为空且指针为零,进行循环,当指针不为零时,则进行遍历操作,再将指针入栈,之后将指针指向该指针的左儿子,设置指针不为零的判断条件,这就意味着,只要它还没有为NULL,指针就会一直向左指,直到遇上叶子,那么,它就开始回朔,这时,回朔可以直接也在循环语句中实现,也就是说,回朔的条件刚刚与刚刚所说的指针为NULL相反,当指针不为NULL时,而栈中还有数据时,则开始访问右儿子,如此循环,则实现了前序遍历的操作顺序,对根指针进行操作,然后,访问左儿子,之后,访问右儿子,下面是代码的实现:
/** * 操作:genealogyNode * genealogy::findParentNode(string findName, genealogyNode * first == root) * 功能:找出name所在的结点,并且返回结点指针 * 前置条件:该类已经被初始化,该树的树根为空 * 输入:一个genealogyNode的指针,用于确定开始寻找的指针,一个string型的name,用作KeyWord * 输出:name的结点指针genealogyNode*,若不存在,返回NULL * 后置条件:让input()操作能够成功完成,让input()函数找到所在结点将内容插入树中。 **/ genealogyNode * genealogy::findParentNode(string findName, genealogyNode * first) { genealogyNode * result = first; // 注意,调入指针的first本身不搜索,搜索方法为,不向上搜索,只向下搜索,并且,广度遍历 first = first -> leftChild; std::stack < genealogyNode* > s; while(first != NULL || !s.empty()) { if (first != NULL) { if(first -> name == findName) { return first; } s.push(first); // 递归模拟,入栈 first = first -> rightChild; // 访问右子树 } else { first = s.top(); // 回朔至父亲节点 s.pop(); first = first -> leftChild; } } return result; }
② 使用了模拟递归后序遍历的方法删除树的叶结点:genealogyNode * genealogy::deleteFunction(void)
实际上,模拟递归后序遍历并没有前序遍历的简单,这是因为,它要在回朔的时候才对指针进行访问功能的操作,而回朔时,如果根结点有左右孩子,那么更是需要考虑的因素了,如何回朔呢?下面的实现方法使用了循环的嵌套,在外循环,它只是负责将指针入栈,至于出栈操作,是由外循环内的一个判断条件来进入内循环进行出栈和访问操作。那么,如何让指针知道在外循环时何时出栈,何时入栈呢?这时可以加入判断条件,判断指针是否叶子结点,只有当其为叶子结点时,则开始访问指针,这时的访问,就实现在后序遍历了,访问后,对指针进行回朔,这时已经进入第二个循环了,直到它为空或者遇上双结点即可结束内循环,回到外循环中判断是否继续循环,这时回到外循环中,只有一种情况会使它继续,那就是,之前访问过同时有左右结点而又只访问了一边的结点的指针。具体的算法及思路可参照下图:
下面是代码的实现:
/** * 操作: void genealogy::deleteFunction() * 功能:将fence结点及fence结点以下的所有结点删除 * 前置条件:fence结点不为空 * 后置条件:fence结点为NULL,原fence结点下的结点被delete. **/ void genealogy::deleteFunction() { if (fence == NULL) return; // 考虑一种情况,其本身即无左右结点 if (fence -> leftChild != NULL) { std::stack <genealogyNode * > s; std::stack <genealogyNode * > tag1; // 本带双孩子的标记 std::stack <genealogyNode * > tag2; // 已访问右结点的标记 genealogyNode * first = fence -> leftChild; s.push(NULL); // 设置这个为从上删除到最上的首位标记 while(first != NULL || !s.empty()) // 用于入栈,停止递归的外循环 { if (first -> leftChild != NULL && first -> rightChild == NULL) { s.push(first); first = first -> leftChild; } else if (first -> leftChild == NULL && first -> rightChild != NULL) { s.push(first); first = first -> rightChild; } else if (first -> leftChild != NULL && first -> rightChild != NULL) { s.push(first); tag1.push(first); tag2.push(first); first = first -> leftChild; } else // 为叶结点 // 用于出栈,操作的主要条件 { bool goOnDel(true); // 判断是否继续删除的标记,遇上双结点时停止 do { if (first == root) // 当删除的是根结点时 { delete root; root = new genealogyNode(NULL, NULL, NULL); goOnDel = false; break; } if (first == first -> parent -> leftChild) // 当其为parent的左孩子 时 { first -> parent -> leftChild = NULL; delete first; } else // 当其不是parent的左孩子时,对本身结点的左边的结点有影响 { genealogyNode * lcToRcNull = first -> parent -> leftChild; while(lcToRcNull -> rightChild != first) { lcToRcNull = lcToRcNull -> rightChild; } lcToRcNull -> rightChild = NULL; // 修改左孩子的指向右边的结点,断开联系 delete first; } first = s.top(); // 回朔到上一结点 s.pop(); if (tag1.empty()) { goOnDel = false; } else { if (first == tag1.top()) { if (first == tag2.top()) // 存在同一结点,证明该双结点的右结点还未被访问 { first = first -> rightChild; tag2.pop(); goOnDel = false; } else { tag1.pop(); // 已访问该双结点的右结点,可以继续向上删除操作 goOnDel = true; } } } if (first == NULL) // 也算是用于结束外部循环的条件,s栈内存放的第一个数据是NULL标记 { if (!s.empty()) { s.pop(); } } }while(goOnDel && !s.empty() && first != NULL); // 删除操作的继续 }// END ELSE // 叶子结点 }// END WHILE 入栈外循环 // 以上操作将该结点以下的操作删除完毕后,该删除本身了 delete first; // fence下的左孩子 } if (fence == root) { delete fence; fence = NULL; root = new genealogyNode(NULL, NULL, NULL); return; } if (fence -> parent -> leftChild == fence) // fence为父结点的左结点,需考虑改变父结点 { if (fence -> rightChild == NULL) // fence右边为空,它为父结点的唯一结点 { fence -> parent -> leftChild = NULL; } else { fence -> parent -> leftChild = fence -> rightChild; } } else // fence 不是父结点的左结点,需要考虑的只是它的邻近的左结点的变化 { genealogyNode * fenceRight = fence -> parent -> leftChild; while ( fenceRight -> rightChild != fence) { fenceRight = fenceRight -> rightChild; } fenceRight -> rightChild = fence -> rightChild; } delete fence; fence = NULL; return; }
③ 加载数据操作的函数实现:genealogyNode * genealogy::load(void)
这里实现加载操作与删除操作有一个相似之处,只是刚刚在实现删除操作时没有提到那思路,加载操作,要实现的最终也只是将数据加载到家谱中,也就是说,这其中有一个插入过程,该插入过程,由于这为孩子兄弟N叉树的实现方法,所以,在插入时,需要考虑两种情况:
第一,要插入的结点的父亲,无左儿子,此时,需要将结点插入到左儿子处,就要修改父亲的结点;
第二,要插入的结点的父亲,有左儿子,此时,需要将结点插入到左儿子的右孩子处,只改左儿子的右结点。
下面是部分代码的实现:
/** * 操作:void genealogy::load(); * 功能:将家谱中的信息保存到用户指定文件当中。 * 前置条件:调入的家谱对象非空。 **/ void genealogy::load() { ...... // 省略 genealogyNode *tempNode = findParentNode(parentName, root); // 先找到正确的位置 fence = tempNode; // 此时fence的值即为父母所在结点 if (tempNode -> leftChild == NULL) { tempNode -> leftChild = new genealogyNode(tempName, tempSex, tempDieDate, tempBornDate, tempBooldType, tempJob, tempRemark, tempNode, NULL, NULL); } else // 当它有兄弟的时候,则需要将临近指针的右指针改变,并且找到最右指针,加入这个家族中去 { fence = tempNode -> leftChild; while (fence -> rightChild != NULL) { fence = fence -> rightChild; } fence -> rightChild = new genealogyNode(tempName, tempSex, tempDieDate, tempBornDate, tempBooldType, tempJob, tempRemark, tempNode, NULL, NULL); } fence = NULL;// 还原fence; } fin.close(); // 最容易遗忘的一步,关闭文件 return; }
④ 判断输入日期是否正确的函数实现:genealogyNode * genealogy::inputDateJudgeHelp(string)
/** * 操作:bool gnealogy::inputDateJudgeHelp(string); * 功能:判断string类型的数据是否为xxxx.xx.xx的日期格式,其中x代表数字 * 前置条件:有genealogy::input()函数,与其结合使用 * 输入:string的数据。 * 输出:返回一个bool类型的值。 * 后置条件:该日期格式及数值符合[yyyy.mm.dd],则返回真,否则,返回假。 * 这使得输入Date后,能够判断是否需要再重新输入正确的Date. * 经过这筛选后,才能成功将该数据输入进genealogyNode中。 * 在该类中的函数input() 和cinOneNodeType()都有用到该函数。 **/
这里的判断日期的实现方式有点特别,因为这时使用的源数据类型是string型,也没有进行转换,直接就用string型的数据对日期进行判断了,这里的判断用户输入的日期输入是否正确,主要有两点得保证,第一点是需要保证它符合yyyy.mm.dd的格式,第二点是保证它的月份,日期,不超出正常的范围。
判断方法也很简单,只需要将其逐个数字比较提取即可,具体算法请看下面的算法盒图:
☞ 总结
这次写这个小程序花的时间比较长,断断续续地用了十三天的时间(现2012.02.03),大概写出了2500行的代码,实现了家谱管理系统的普通功能。
这次在制作程序的过程,大概也是可将其划分为五个步骤,准备 -> 设计 -> 代码编写 -> 再设计 ->代码编写继续 -> 程序调试 -> 资料整理及说明书的编写。
首先是“准备”阶段,这次的准备阶段主要做了资料收集查询的工作,先是上去百度找了一下大概家谱是要注意些什么,什么是家谱,了解一下关于“家谱”的背景,查阅资料后,知道了家谱大概可以使用树的结构来实现。
然后,进入“设计”阶段,在代码的编写之前,往往需要经过很长的一段设计时间,这是因为,如果设计得好的话,就可以为以前的代码编写和调试省下很多的时间了,通过设计,还可以很大部分地决定了你的代码的质量,在设计阶段,我并没有立即将程序的全部过程什么的闭卷就做了起来,这次,由于不是很想胡乱设计,于是,开始在设计的同时学习UML的内容,只是,学到到最后,虽然把整个程序的结构稍写出来了,但是帮助不大,因为没有怎么用上UML的内容,因为这个小程序有点小型,只是,在没有完全做好设计工作的这时,迫不急待地进入了下一阶段了。
之后,进入了代码编写的阶段,这次的代码写得很快,因为差不多全部的函数都有了框架,只是,在写的时候,开始出现问题了,之前因为浮燥而没有设计好的小算法,这里开始卡住,得慢慢想,慢慢从原代码上补,只是,在补的同时,整个程序跟刚刚开始设计时又有点不同了,补的功能越多,跟原先的设计就产生越大的差异,于是,我抛弃了原来的程序设计图,抛弃了用不上的UML知识,重新将程序设计一次,使用了只要是人就能看得懂的算法流程框图,还将各个类之间的调用函数关系重新列了出来,终于,经过再设计再编程后,代码编写阶段完成了。
最后,进入程序“程序调试”阶段,在这一阶段,是我写四个较为像样点的程序以来使用的调试时间最短的一次,只解决了三四个小问题就完成了这个调试阶段了,而且,基本上都没有出现语法错误,一般只是逻辑错误,在这次调试过程,主要使用了中断,单步调试等软件基本功能,调试起来很方便。
在程序能够完全运行起来的那一刻,心情无比的兴奋,然后知道只需要次说明书写完了,这个程序就可以基本完工了。这次的说明书写得比较慢,与之相比不同的是,这次的说明书,画了几副图,画图所需时间比较长。
从这次的程序编写中,我知道了,一个良好的设计阶段真的是非常重要,正是因为那个设计不好,才令自己有重新设计的冲动,在重新设计以后,全部流程都清晰了,由于代码量大,有时候如果没有设计的话,自己写到哪一步都会很容易忘记的,如果设计了,就可以避免这一事情的发生,在代码量大的时候,也可以知道自己写到哪一个小步骤,写完这一个小步骤后,可以清晰地返回继续往下一步骤编写。
在这次的程序编写时,我也找回了遗失了很久的编程手感了,也找回了那编程的快感了,有空就找个小程序练练手,真好玩!噢~顺便地说,本来对树的内容是十分陌生的,本次选题正是因为对链表太熟悉了,所以才不选链表做题目的,越是不熟悉的内容,就越是要想去做,于是,边做边想这个树的东西,发觉原来树的内容也是不难的,也是将指针指来指去罢了。
而且,这次的程序编写中,也有了一个新的体会,精简一点说,就是,编程,思想,语言,当思想超越了语言的时候,就会感觉到语言的基础不好,相对地,当语言超越了思想的时候,你使用起你的思想的时候就会得心应手了,两者,相互补充,当两者都强化的了时候,基本上小程序来说,就完全不是问题了。
☞ 附A:程序制作过程记录
第01天:(2012.01.19)
这一天,忽然想开始做做这个大作业,一看题目,分析了题目的功能要求,却不想下手马上编程,不想再像以前那样在编程前做太少的准备工作,导致整个软件写得那么烂,逻辑那么乱,于是,想去学习UML,了解一下到底这样的软件编写,到底要怎样一步步实现的,翻开了《UML和模式应用》机械工业出版社,一看,就看了几个小时,越看越兴奋,这本书写得实在是太好了,让人可以真正地理解到什么是OOA/D(Object-oriented analysis和Object-oriented design),这本书还让人知道整个软件开发的流程是怎样的,这让人知道自己大概在软件开发时起到了什么样的作用,还让人知道具体如何是开发一个软件,太好啦,还有这种思维,这种面向对象的思维在处理其它方面问题的时候照样适用。这本书,其实内容并不多,第一天就看了六章了,知道了大概整个流程就是 初始 -> 细化 -> 构造 -> 移交。然后,很喜欢其中的迭代思想,在UP里面得到很好的应用,要放弃瀑布式开发,于是,我开始认真地看了第六章,初始,虽然,当中对于这样的小程序能用上的地方不多,可能只能用上”FURPS+”中的其中一个”F”,但是部分这种思想还是对软件开发很有帮助,于是,就把它看下去了。
第02天:(2012.01.20)
将Genealogy Management System的用例写出来了,不过感觉真的不好写,没有书上的项目例子大型,可以的东西也不多,也没有项目的那么多迭代,可能只能向书上不推介的方法进发了,瀑布式建模,因为,还没有到细化阶段,好吧,接下来看看这个细化阶段,迭代,究竟是什么样的东西。
第03天:(2012.01.22)
刷了一下《UML》中的细化-交互图的内容后,发觉,确实没有什么必要为这个小程序画交互图,规模小了点,不是那么好画,仅仅有两个类,而且一个还是要结点类,两个类就能完成的所谓的项目,晕,有点纠结,但是,又好像挺好玩的,于是,还是决定把这个交互图画出来了,虽然不知道这个称不称得上叫交互图,符不符合规格标准,嘿嘿,不过,画了出来以后发觉还是对自己编程挺有帮助的,思路非常清晰。
第04天:(2012.01.25)
今天尝试画UML类图,然后,在画UML类图的时候,发觉很多东西还没有在system sequencediagram和domain model中画出来,于是,决定,先不修改domain model,尝试直接先修改system sequence diagram,然后看要不要对domain model 进行修改,这可能是刚刚开始的时候,建UC建得不好,导致后面一步步地问题就逐渐体现出来了,不过,也可能这是到了必须经过的细化阶段。后来稍想了一下,原来,改动也不算是很大,于是,简单地将各图改了一下,主要改动还是UML类图。准备工作已经差不多了,可以正式开工了。
第05天:(2012.01.26)
开始写主函数及各个类函数,将主函数的菜单及整体框架定下,将genealogyNode.h类和genealogy.h类写出。
第06天:(2012.01.27)
花的时间有点少,只是完成了input()插入的功能,不过,在写的时候发觉,还是不够,没有将算法的流程图画出来,逻辑有点乱。
第07天:(2012.01.28)
将input()可以编译通过。和部分的update()修改功能。
第08天:(2012.01.29)
重新将各个函数的关系画出来,这次认真地细想到每一个算法的细节,组织好逻辑,然后再编写代码,不想再出现前两天那样编码,边写边想,然后不够函数又要再补,这样不断地打补丁,不仅搞得函数里面乱糟糟的,还让自己觉得这个class变繁琐了,所以,还是决定重新理清思路,而且,这次要再深入地写出算法图,以前的那几副实在太不像样了,写出来的不大实用,这样编程编下去越写越痛苦,像陷入了迷宫里面一样,不断地找路,找路,效率超低。这次用手写草图算法和功能都混合的自己看得懂就OK了,不用什么规范与格式了,UML的知识,如果要熟练,还需要再找一本书,做多个小项目的书,不断地练习才能熟练地运用,这次简单的UML的学习,其中最大收获就是让我知道了,分清哪个功能在哪个.h中实现是很重要的。竟然连续十天都还没写完这个小程序,晕啊,历时最长的一次了,不知道是不是因为断断续续所以有这样的现象,这次,给自己限定时间,2012.02.05前,要将其完成。Promise, Yes, Promise.还有7天的时间,把这个玩完。话说,今天在想要实现删除功能的时候,还顺便把树的后序遍历的非递归方法想出来了,原来它是比前序遍历麻烦一点。
第09天:(2012.01.30)
除了文件的输入输出功能没有实现外,基本上将其它功能的代码写出来了。
第10天:(2012.01.31)
实现了文件的输入输出功能,全部函数编写完成,开始进入调试阶段,不过,这时,出现了一个错误,设计程序时,将一个string的知识点搞错的,当用户输入string2010.12.12时,string正确是[0][1][2][3][4][5][6]...这样,可是在编程时,将它反转了,于是,要检查所有的相关函数,这时,由于函数间的关系很复杂,光使用之前的功能草图好像不能完全看出它们间的关系,于是,决定了先将全部的函数关系图列出来,方便调试检查解决。
第11天:(2012.02.01)
进度比想象中快了N倍,这天早上,所有的调试工作完成了,调试过程中,几乎都只是一些双等号啊,漏了某些指针的指向之类的一些小错误,没有其它的大问题,这是写这么多次这种程序以来调试最顺最快的一次,果然,前期工作起了相当大的作用,下午,开始编写说明书了,最轻松愉快的最后一步了,程序完成的那一刻的心情,超爽,超兴奋!好久没有试过编程后有这样的感觉了,写个小小的程序,终于让我在这过程中找回了编程的快乐!
在写说明书的时候,我在想一个问题,这次的说明书,该用哪些图呢?UML类图,只有两个类,不大合适,domainmodel? 它们之间的关系不算太复杂,画了也是白画。user case? 用户案例,跟用户的功能演示差不多,没有这个必要,嗯,先把用户功能演示放在说明书较前的位置吧,另外再为这个功能演示画一个用户看的程序功能图,为了表达在程序间各个类,各个子函数之间的关系,好吧,决定了,试度程序包图,说不定能把想要表达的函数包含关系表达得比较清楚,最后,还需要为一些比较麻烦的算法的子函数弄一个盒图,嗯,也就是说,一共三图,一,功能图,二,程序包图,三,算法流程图。
第12天:(2012.02.02)
做着做着,发觉也就那么几个功能,没有什么好做的,于是,功能图也不做了。结果做了个用户功能演示说明,这跟User Case的主场景差不多,应该说,几乎一样,只是没有将计算机和用户分得这么清罢了。
忽然发觉包图也不是这个概念,不是我想表达的意思能用到的图,于是,连UML包图也放弃了,因为不知道用些什么图来表示才更好,又不是很想直接自己画,觉得很不规范,于是,再次翻开《UML》,快速回顾翻阅,发觉好像真的没有很多内容适用于这么一个小程序,好吧,还是自己仿造这个包,然后,试试画一个容易理解的图出来吧,至于UML这些,以后肯定会提高的!
第13天:(2012.02.03)
写出说明书中各个类实现功能的方法,画出算法的流程图,包适后序遍历的非递归实现的盒图和判断日期正确与否的盒图,最后是把说明书的总结,排版,文件的整理,完工!