结对项目:一个自动生成小学四则运算题目的命令行程序(c++)
结对编程
这个作业属于哪个课程 | 软件工程 |
---|---|
这个作业要求在哪里 | 作业要求 |
这个作业的目标 | 实现一个自动生成小学四则运算题目的命令行程序,以及对给定的题目文件、答案文件有统计对错功能。 |
成员 | 3118005386 吴永力 、3118005382 王舜鑫 |
作业Github地址
- GitHub链接
- 可运行PrimaryArithMetic.exe已放在Release文件夹上
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 60 |
Development | 开发 | 800 | 1125 |
· Analysis | · 需求分析 (包括学习新技术) | 120 | 120 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审 | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 15 |
· Design | · 具体设计 | 60 | 180 |
· Coding | · 具体编码 | 400 | 600 |
· Code Review | · 代码复审 | 50 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 60 |
Reporting | 报告 | 130 | 110 |
· Test Report | · 测试报告 | 60 | 60 |
· Size Measurement | · 计算工作量 | 10 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 30 |
· 合计 | 990 | 1295 |
效能分析
在-n 10000 -r 100 5 100 前提下进行测试
CPU时间占比:
函数调用占比:
可以看到主要时间都集中在计算式子计算过程不会出现负数,除以0以及生成的式子等价的函数上,甚至大量的string操作符上。
一开始用生成的式子与之前生成的式子进行等价判断,导致了时间复杂度有O(n!),程序会卡很久才可以生成,后续优化了算法就可以快速生成。(当答案相等时候才进行等价判断)
设计实现过程
-
通用数类:观察题目发现要表示分数以及所有整数也可以用分数来表示,即实现一个通用的类Frac就可以实现全部数一起用
class Frac { public: Frac(); Frac(long long molecule, long long denominator); Frac(std::string ex); ~Frac(); Frac operator+(const Frac& right) const; Frac operator-(const Frac& right) const; Frac operator*(const Frac& right) const; Frac operator/(const Frac& right) const; bool operator==(const Frac& right) const; bool operator!=(const Frac& right) const; //化简 void Simplify(); //最大公因数 long long MaxDiviSor( long long a, long long b); //转换为字符串 std::string to_string() const; private: long long m_Up; //分子 long long m_Down; //分母 };
-
题目生成
-
随机生成一定数量的操作符(+ - x ÷),数量范围在1~3;
-
根据范围参数生成以及操作符数量生成随机指定数量的数(分成整数和真分数)
-
根据操作符数量来生成括号
- 操作符为1个:即不产生括号
- 操作符数量为2个:若生成的括号数量为1个时候,根据随机位置来决定括号的位置(如:(1+2)+3以及1+(2+3) );若为0个,即不产生括号
- 操作符数量为3个:若生成的括号数量为2个时候,括号直接放在两侧(如:(1+2)+(3+4) );若为一个时候,即根据随机数来决定放在右侧还是左侧;若为0个,也是不产生括号。
-
判断式子是否有计算过程有负数以及除以0的情况;若存在,则重复上述步骤。
--根据式子的后缀表达式生成子表达式,再根据子表达式计算判断是否有计算过程有负数以及除以0
-
判断式子是否与之前生成的式子存在等价的情况;若存在,则重复上述步骤。
--根据式子的后缀表达式生成子表达式,再根据子表达式来进行判断式子是否存在等价
-
若生成成功,即可以生成答案并把题目和答案存起来。
-
-
计算结果
- 首先把式子规整化,如空格剔除,以及把 ÷转换为# (因为单字符匹配不了÷)以及补零(如:\(-2+3\) -> \(0-2+3\) ; \(1+(-2*3)\)-> \(1+(0-2*3)\))
- 然后把中缀表达式转换为后缀表达式,若数值为真分数即把真分数直接当作数字存起来
- 最后根据后缀表达式来计算结果
关键函数
void CreateAriTitlesAndAnswer(int titleNums = 10, long long naturlrange = 10, long long fracrange = 5, long long downrange = 10);
- 参数:题目数量,自然数范围,真分数范围和分母范围
- 具体功能:生成题目
bool Cmp_ExChild(std::vector<std::string> ex1, std::vector<std::string>ex2);
- 参数:子表达式1的数组和子表达式2的数组(数组索引:0:操作数1 ;1:操作数2;2:运算符)
- 返回值:bool
- 具体功能:判断两个子表达式是否等价
bool IsSimilary(std::string scource,std::string destination);
- 参数:原表达式和要比较的目的表达式
- 返回值:bool
- 具体功能:判断两个表达式是否等价
std::string SuppleInfix(std::string infix);
- 参数:中缀表达式
- 返回值:字符串
- 具体功能:规整化中缀表达式
bool ReversePolish(std::string infix);
- 参数:中缀表达式
- 返回值:bool(表示是否转换成功)
- 具体功能:生成后缀表达式并存起来
std::string GetResult(std::string infix);
- 参数:中缀表达式
- 返回值:计算结果的字符串
- 具体功能:输入中缀表达式即可得到计算结果
std::vector<std::vector<std::string>> GetChildExpression(std::stack<std::string> reversePolish);
- 参数:后缀表达式
- 返回值:子表达式的二维数组
- 具体功能:根据后缀表达式得到一个子表达式的二维数组
关键代码展示
-
生成题目部分
void PriAriCreator::CreateAriTitlesAndAnswer(int titleNums, long long naturlrange, long long fracrange, long long downrange) { for (int i = 0; i < titleNums;) { int opNum = random_Int(1, mOpNumMax); std::vector<Frac> numbers; for (int i = 0; i <= opNum; ++i) { //产生随机数(整数还是分数) int isCreate = random_Int(0, 1); if (isCreate == 0) { long long num_R = random_LL(1, naturlrange); Frac frac(num_R, 1); numbers.push_back(frac); } else if (isCreate == 1) { long long num_2 = random_LL(1, downrange); long long num_1 = random_LL(1, fracrange * num_2); Frac frac(num_1, num_2); numbers.push_back(frac); } } std::vector<std::string> ops; //随机生成符号 for (int i = 0; i < opNum; ++i) { std::string op = random_OP(); ops.push_back(op); } int bracketNum = random_Int(0, opNum - 1); std::string infix = ""; if (opNum == 1) { infix += numbers[0].to_string() + ops[0] + numbers[1].to_string(); } else if (opNum == 2) { if (bracketNum == 1) { int position = random_Int(0, 1); if (position == 0) { infix += "(" + numbers[0].to_string() + ops[0] + numbers[1].to_string() + ")"; infix += ops[1] + numbers[1].to_string(); } else { infix += numbers[0].to_string() + ops[0]; infix += "(" + numbers[1].to_string() + ops[1] + numbers[2].to_string() + ")"; } } else if(bracketNum==0) { infix += numbers[0].to_string() + ops[0] + numbers[1].to_string() + ops[1] + numbers[2].to_string(); } } else if (opNum == 3) { if (bracketNum == 0) { infix += numbers[0].to_string() + ops[0] + numbers[1].to_string() + ops[1] + numbers[2].to_string()+ops[2]+numbers[3].to_string(); } else if (bracketNum == 1) { int position = random_Int(0, 2); if (position == 0) { infix += "(" + numbers[0].to_string() + ops[0] + numbers[1].to_string() + ")"; infix += ops[1] + numbers[1].to_string() + numbers[2].to_string() + ops[2] + numbers[3].to_string(); } else if (position == 1) { infix += numbers[0].to_string() + ops[0]; infix += "(" + numbers[1].to_string() + ops[1] + numbers[2].to_string() + ")"; infix += ops[2] + numbers[3].to_string(); } else if (position == 2) { infix += numbers[0].to_string() + ops[0] + numbers[1].to_string(); infix += ops[1] + "(" + numbers[2].to_string() + ops[2] + numbers[3].to_string() + ")"; } } else if (bracketNum == 2) { infix += "(" + numbers[0].to_string() + ops[0] + numbers[1].to_string() + ")"; infix += ops[1] + "(" + numbers[2].to_string() + ops[2] + numbers[3].to_string() + ")"; } } if (IsCreaateNegative(infix) || IsDownZero(infix)) //过程不能有负数以及除0 { continue; } std::string answer = mCalculator.GetResult(infix); bool flag = false; for (size_t i = 0; i < mAnswers.size(); ++i) { if (mAnswers[i]._Equal(answer) && IsSimilary(infix, mInfixs[i])) { flag = true; break; } } if (!flag) { mInfixs.push_back(infix); mAnswers.push_back(answer); ++i; } } SaveTitleToFile(); SaveAnswerToFile(); }
-
后缀计算式子结果
std::string Calculator::CalcuReversePolish() { if (number.empty()) { return ""; } std::stack<Frac>* temp = new std::stack<Frac>; std::string str; while (!number.empty()) { str = number.top(); number.pop(); if (!IsOp(str)) { Frac frac(str); temp->push(frac); } else { if (temp->size() > 1) { Frac first, second; first = temp->top(); temp->pop(); second = temp->top(); temp->pop(); char c_op = str.at(0); switch (c_op) { case '+': second = second + first; temp->push(second); break; case '-': second = second - first; temp->push(second); break; case '*': second = second * first; temp->push(second); break; case '#': if (first == Frac(0, 1)) { return std::string("X"); // X代表无结果 } second = second / first; temp->push(second); break; default: break; } } } } Frac result = temp->top(); temp->pop(); delete temp; return result.to_string(); }
-
判断式子相似
bool PriAriCreator::IsSimilary(std::string scource, std::string destination) { //获取后缀表达式 std::stack<std::string> RP1 = mCalculator.GetReversePolish(scource); std::stack<std::string> RP2 = mCalculator.GetReversePolish(destination); //得到子表达式 std::vector<std::vector<std::string>> ex1 = mCalculator.GetChildExpression(RP1); std::vector<std::vector<std::string>> ex2 = mCalculator.GetChildExpression(RP2); for (auto& p : ex1) { for (auto& v : ex2) { if (Cmp_ExChild(p, v)) { return true; } } } return false; }
效果展示
-
题目生成
在 -n 10000 -r 50 5 10情况下
部分题目:
1: 1 * 10 = 2: 19 ÷ 5 = 3: 40 * 3'3/4 = 4: 21 ÷ 22 = 5: 28 * 17 = 6: 5 ÷ 1/3 = 7: 5 + 3'3/10 + 4 ÷ 4 = 8: 38 + 2'3/8 = 9: 4'1/9 + 27 = 10: 46 * 3 ÷ 30 * 2'2/3 = 11: (4'2/7 ÷ 3) + (21 ÷ 11) = 12: (2 ÷ 14) + (21 + 4'1/3) =
以及答案:
1: 10 2: 3'4/5 3: 150 4: 21/22 5: 476 6: 15 7: 9'3/10 8: 40'3/8 9: 31'1/9 10: 12'4/15 11: 3'26/77 12: 25'10/21
-
验证答案
因为全部正确,所以只贴一部分出来了:
Correct:10000(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,.......
单元测试
单元测试:对一定情况下的题目生成和自身验证
共有以下组:
样例 | |
---|---|
1 | -n 10 -r 10 |
2 | -n 100 -r 100 |
3 | -n 100 -r 10 |
4 | -n 1000 -r 1000 |
5 | -n 1000 -r 10 6 10 |
6 | -n 1000 -r 10 6 50 |
7 | 验证正确的文件(含错误答案) |
8 | 验证不存在的文件 |
9 | -n 10000 -r 10 |
10 | -n 10000 -r 100 |
部分代码:
TEST_METHOD(TestMethod9)
{
PriAriCreator* Par = new PriAriCreator();
Par->CreateAriTitlesAndAnswer(10000, 10);
Assert::IsTrue(Par->ReadTitilesAndAnswer("Exercises.txt", "Answers.txt"));
if (Par->ReadTitilesAndAnswer("Exercises.txt", "Answers.txt"))
{
Par->VerificationAnswer();
}
delete Par;
}
TEST_METHOD(TestMethod10)
{
PriAriCreator* Par = new PriAriCreator();
Par->CreateAriTitlesAndAnswer(10000, 100);
Assert::IsTrue(Par->ReadTitilesAndAnswer("Exercises.txt", "Answers.txt"));
if (Par->ReadTitilesAndAnswer("Exercises.txt", "Answers.txt"))
{
Par->VerificationAnswer();
}
delete Par;
}
测试结果:
不得不说:c++还是挺快的
项目小结
分享经验,总结教训
要仔细查看题目理解要求,不要一概而论,不然容易产生二义性导致多余重复的工作;
在实现某些算法时候,应注意其的时间复杂度,更注意下有无更好的实现方法,所以对于算法部分仍要加强学习;以及写代码注意代码复审,可以减少一定的Debug时间。
这是第一次合作编程,我们通过代码和注释表达我们对这次作业的想法,这是一种很好的交流方式。在开发种遇到问题,我们及时地交流反馈,认真聆听对方的意见,这让我们可以很好地解决困难。
成败得失
- 未实现图形界面(有想法用QT,但时间不够放弃了)
- 还有有一定的代码耦合度,以后得继续学习写耦合度低的代码。
结对感受
结对感受 | 对彼此的闪光点或建议 | |
---|---|---|
吴永力 | 这次结对编程,有同伙的帮助,帮助让我更好的理解了题目的要求以及更好地开发接口模块,有小伙伴(或团队)有时候可让开发效率更高。 | 有一定的学习能力以及题目理解能力。 |
王舜鑫 | 这次结对编程,有个厉害的同伴的帮助,让我自身的能力有了一定的提高,也让我节省了许多查找资料的时间,对比之前,学习效率高了很多,团队的力量就是如此强大。 | 学习能力强,很强的责任感,很强的团队精神。 |