作业信息
教学班级:周五班
项目地址:https://github.com/buaddd/buaa_SE_2022_Pair_Program
PSP表格
Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|
计划 | 60 | 120 |
估计这个任务需要多少时间 | 30 | 30 |
开发 | 1200 | 1500 |
需求分析 (包括学习新技术) | 180 | 120 |
生成设计文档 | 180 | 120 |
设计复审 (和同事审核设计文档) | 30 | 60 |
代码规范 (为目前的开发制定合适的规范) | 30 | 60 |
具体设计 | 180 | 180 |
具体编码 | 600 | 600 |
代码复审 | 180 | 120 |
测试(自我测试,修改代码,提交修改) | 300 | 300 |
报告 | 180 | 180 |
测试报告 | 120 | 120 |
计算工作量 | 30 | 60 |
事后总结, 并提出过程改进计划 | 60 | 60 |
合计 | 56 * 60 | 60.5 * 60 |
项目设计与实现
项目结构与UML图
输入输出,建图,异常检查
本项目采用面对对象的思路进行设计,将不同的部分封装成不同类,结构如下所示
- MyIO: 负责文件的读取与计算结果的输出,保存读入的所有字符串
- Node, Word_vertex: 分别针对不允许环路与允许环路的情况,保存单词相关的信息,例如首尾的字母,后继节点等内容
- Generator:根据字符串与参数情况生成Node与Word_vertex对象,建图
- Checker: 进行环路检测,对于非-r的情况,如果检测到了环路,需要退出程序并报错
Core相关计算类
- Core: 封装本次作业的计算接口
- Node_chain_builder与Word_chain_builder: 负责构建单词链,使用graph中的信息,构建单词链
- Node_graph与Word_graph:保存所有节点,存储相关的信息,供生成者调用,辅助生成单词链
- Node_chain与Word_chain:存储计算得到的单词链结果
- Word_tarjan_vertex:代表计算过程中某一类Word_vertex的特殊情况
UML图
接口设计与实现
Information Hiding,Interface Design,Loose Coupling
- Information Hiding:类只开放对应的功能接口,但隐藏其数据和实现细节。
- Interface Design:一个好的接口设计可以使项目的结构更加条理有序。
- Loose Coupling:处理好类与类之间的通信,避免出现一个类直接出现在另一个类中来及进行数据的交换
计算模块接口的设计与实现过程
本次作业要实现的计算模块接口如下:
int gen_chain_word(char* words[], int len, char* result[], char head, char tail, bool enable_loop);
int gen_chains_all(char* words[], int len, char* result[]);
int gen_chain_word_unique(char* words[], int len, char* result[]);
int gen_chain_char(char* words[], int len, char* result[], char head, char tail, bool enable_loop);
我们将其分为无环与有环两种情况去处理:
- 对于无环的情况,由于没有环路出现,因此可以将所有类似a…b结构的单词归纳为一类,并存储在Node类中,同时建图过程中存储每个Node的后继Node。
- 对于有环的情况,上文所述的同结构单词可以重复利用,因此不能再压缩所有单词,需要对每一个单词生成一个Word_vertex类,同时建图过程中存储每个Word_vertex的后继Word_vertex。
针对Node类和Word_vertex类,我们分别设计Node_chain类和Word_chain类来封装两种数据类的链信息,并设计Node_chain_builder类和Word_chain_builder类来实现两种链的构造。
对于接口实现的4种功能,gen_chain_unique 和 gen_chains_all 仅涉及无环情况,只需要调用 Node_chain_builder 类;而 gen_chain_word 和 gen_chain_char 既设计无环情况,又涉及有环情况,需要根据需求选择调用Node_chain_builder类和Word_chain_builder类。
两种builder类的实现结构基本类似,包括init()、build()、get_result()和get_num()这4个基本功能,分别用于初始化、构造对应chain、返回result结果和返回result数组大小
其中,build()内调用一个私有成员函数travelsal_build()来递归遍历每一个节点(Node节点或者Word_vertex节点),每当遇到一个正确的可结束的结尾时,调用相应的update函数来更新目标结果
计算模块接口部分的性能改进
针对无环和有环两种情况,分别给出优化策略
- 无环的情况:我们使用Node代替Word_vertex来保存单词信息,通过单词的首尾字母在一个26*26的DAG图(类似a…a的情况单独处理)上找到入度为0的点,然后向下遍历。对于gen_chain_word、gen_chain_char和gen_chain_word_unique这种查找最大单词(或字母)数的问题,我们只需要在遍历到出度为0的点时进行更新。经过优化,每个节点只会计算一次。而对于gen_chains_all保存所有单词链的问题,需要进行进一步的优化。
- gen_chains_all的优化:同样在26*26的DAG图上找到入度0的点向下遍历,每当chain加入一个新的Node时,我们根据cur_chain_all的内容和新节点Node更新cur_chain_all。如果新节点Node的出度等于1,继续向下遍历;如果新节点Node的出度大于1,我们将cur_chain_all压栈,压入cur_chain_all_list,并把cur_chain_all清空,继续向下遍历;如果新节点Node出度等于0,则根据cur_chain_all和cur_chain_all_list栈,生成从开始节点到当前节点的所有单词链。经过优化,每种单词链只会被计算一次。
- 有环的情况:我们使用Word_vertex来保存单词信息,此时问题转变为”在一个有正环的有向图上求最长路“,是一个典型的NP-Hard问题,不能在多项式时间内被求解。但是我们可以根据无环的思路来优化有环的情况,首先利用tarjan算法进行多点,将Word_vertex节点划分为若干个强连通分量,我们用Word_tarjan_vertex类进行保存,这些由强连通分量(Word_tarjan_vertex)构成的新图是一个DAG图。我们求新DAG图的逆拓扑序列,并按照逆拓扑序列遍历每个强连通分量内的最长路径,并保存每个节点开始的最长路径。因为我们是按照逆拓扑序列遍历每个强连通分量,所以一个强连通分量内如果遍历到其他强连通分量的节点,那个节点的最长路径信息已知,避免了重复计算,从而实现了优化。当然最坏情况和直接遍历所有节点一致——只存在一个强连通分量。
Design by Contract,Code Contract
在语言层面上检查precondition前置条件、postcondition后置条件以及Assert断言,和单元测试相关。虽然我们在工程中没怎么得到实现(笑)
测试
计算模块部分单元测试展示
测试数据构造:测试数据构造的思路主要是考虑到覆盖不同的情况,没有追求复杂及较为考验时间复杂的的数据,按照不同参数的分类依次进行测试,例如-n,-m, -w, -w -h a, -w -r等等。同时每一个分类下准备类型不同的测试数据,例如-w -h a这个参数组合准备了如下数据
input: {"bbc", "bbd", "beg", "ccf", "cccccf", "grs", "deg"}
output: null
input: {"abc", "bbd", "ccf", "deg", "grrrrx", "xtq", "qbs"}
output: {"abc", "ccf"}
异常处理
项目中遇到异常时,采用输出异常类型至终端并结束程序运行的方式,具体的异常类型如下表所示
异常类型 | 示例 | 异常描述 |
---|---|---|
缺少参数 | Wordlist.exe -r “input.txt” | 此时只有-r参数,没有-m, -n -w ,-c中的任一参数,不会有输出结果,因此缺少关键的参数 |
错误参数组合 | Wordlist.exe -n -r “input.txt” | -n与-r两个参数不能同时存在,此时为错误参数组合 |
重复参数 | Wordlist.exe -n -n “input.txt” | -n与-n两个参数相同,此时为重复参数 |
错误参数类型 | Wordlist.exe -x “input.txt” | -x参数不存在,此时为错误参数类型 |
文件名错误 | Wordlist.exe -r -w “wrong_file.txt” | 在尝试读取文件时发生错误,该文件不存在 |
含单词环 | 输入文本包含abc, cde, eba | 在参数没有-r的情况下,出现了单词环,此时为错误输入 |
无有效单词 | 输入文本为 12213,2323123,2144 | 此时无法解析出有效的单词,因此无法构建单词链 |
无单词链 | 输入文本为 abc, abd | 此时无法构建出单词链,应当提示文本中无单词链 |
单词链长度过大 | -n情况下输出超过20000 | 此时应直接输出长度,并提示超过最大范围,随后返回 |
环路检测实现
异常的检测大多数通过参数直接判断即可,环路检测采用了拓扑排序去判断
bool judge_circle() {
int count = 0; //用于计算是否还有节点剩余
queue<Node*> node_queue;
//找出所有入度为0的节点
for (vector<Node*>::iterator it = nodes.begin(); it != nodes.end();it++)
{
Node* node = *it;
if ((node->get_inDegree()) == 0) {
node_queue.push(node);
count++;
}
//同时判断是否存在多个形如a....a的单词,若存在则必定有环
if (node->get_start() == node->get_end()) {
vector<string*> temp_vector;
node->get_list(temp_vector);
if (temp_vector.size() > 1) {
return false;
}
}
}
//cout << "start" << endl;
while (!node_queue.empty()) {
Node* temp = node_queue.front();
//temp->show_words();
node_queue.pop();
char end = temp->get_end();
//当前节点有后继节点
if (start_map.count(end) > 0) {
vector<Node*> list = start_map[end];
for (vector<Node*>::iterator iter = list.begin(); iter != list.end(); iter++) {
Node* node = *iter;
node->sub_inDegree();
if (node->get_inDegree() == 0) {
//cout << "next node: " << endl;
//node->show_words();
node_queue.push(node);
count++;
}
}
}
}
//cout << count << endl;
return count == nodes.size();
}
关于结对编程
结对过程
别人加我
结对图像资料
《最后的挣扎》
结对编程优点与缺点
- 优点:思路开阔
- 缺点:时间不合适,分工不合适
成员的优点与缺点
姓名 | 优点 | 缺点 |
---|---|---|
戴啸天 | 思路清晰,善于表达自己的想法,能够把握大局 | 不怎么干活,特殊情况太多 |
徐柯轩 | 完成了主要编程的工作,做题有一些神奇的点子,比较重视细节 | 思路比较混乱,没怎么写博客,没学会dll封装(难受) |