2019/10/10 update:式子生成部分已经用二叉树重写掉,简洁好用,能覆盖所有可能的括号位置。
1. github地址:https://github.com/CourierLo/ExercisesForPupils
2. PSP表格:
|
Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) | |
| Planning | 计划 | 60 | 120 | |
| Estimate | 估计这个任务需要多少时间 | 10 | 5 | |
| Development | 开发 | 1030 | 1185 | |
| Analysis | 需求分析 (包括学习新技术) | 30 | 30 | |
| Design Spec | 生成设计文档 | 20 | 15 | |
| Design Review | 设计复审 (和同事审核设计文档) | 30 | 0 | |
| Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 10 | 0 | |
| Design | 具体设计 | 60 | 120 | |
| Coding | 具体编码 | 900 | 1020 | |
| Code Review | 代码复审 | 40 | 10 | |
| Test | 测试(自我测试,修改代码,提交修改) | 60 | 50 | |
| Reporting | 报告 | 120 | 120 | |
| Test Report | 测试报告 | 30 | 10 | |
| Size Measurement | 计算工作量 | 5 | 5 | |
| Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 10 | 10 | |
| 合计 | 1475 | 1515 |
3. 效能分析:假设生成n条表达式,随机生成二叉树O(n);检查重复式子上,复杂度是O(n log(n)) (常数忽略,应该比较大),map容器的空间复杂度我不会分析。但是,只要n足够大,比如一百万以上,就会Memory Limit Exceeded,开不了那么大空间;其后是栈的top(取栈顶)操作,约占5%的时间。C++的输入输出流慢得很,尤其是输出到控制台,很多时间会花费在IO上,但是输出到文件上就问题不大,这样IO时间会很小,占不到1%。生成10000条式子程序能1~2s内跑完,但是Visual Studio生成vsp报告要等一会儿。
①生成表达式并计算答案(vector版:可以看到在n = 10000, range = 50要去掉很多重复的表达式(综合约占35%),如果将range变大重复式子会变少,也许用更好的随机函数会减少重复?)
a. 暴力模拟表达式,vector insert版

b. 用二叉树生成,顺便优化空间(不再使用vector存储式子和答案,直接输出)

可以看到总时间少了一半,优化效果拔群。
②计算并检验答案文件,没有变动

4. 设计实现过程:
①涉及到分数、整数和运算符的模拟运算,我们应该如何尽可能减少字符串的操作呢?将分数、整数、运算符封装成一个类,创造出一种新的(假的)数据类型,配合上C++的重载运算符功能,重新定义符号运算,如此一来就可以直接使用+-*/对类对象进行与整数无异的四则运算。这样做还有一个好处,如果后续要添加运算符,如指数运算,直接在类里面添加operator character就可以,计算过程稍加修改就能用了,没有繁琐的字符串操作方便的很。
②先用二叉树随机生成表达式,计算之后再以中缀表达式的模式输出二叉树形式的表达式串,这种写法意味着你得生成二叉树,然后要写一个是否输出括号的函数(不好的地方是要分类讨论,情况较多,好的地方是生成的式子没有多余括号),然后递归地遍历整棵树。具体可以参考:https://blog.csdn.net/kuku713/article/details/12684509 还有 https://juejin.im/post/5c3a2ab06fb9a049dd8084be。
③计算运算式子,很简单的啦。用辅助栈就能将中缀表达式转化为后缀表达式,还能用栈计算后缀表达式的值。用二叉树也行,很秀但没必要,反正时间复杂度都差不多,栈很简单就能实现了.可以查阅《数据结构与算法分析 C语言描述》。
④如何使得产生的式子的运算路径不同?既然要求路径不同很自然就会想到树结构,我这里偷懒了直接用C++的map容器(map<string, bool>,内部红黑树),将数字路径转化为字符串打标记,一旦有两个字符串路径一模一样就重新生成一个就好了。时间复杂度O (n log(n)),最多要我生成1e4个嘛,map的内存和空间都绰绰有余了,如果要生成更多式子比如说1e6个,还是老老实实用字典树好过,O(n)的。然而这是个假算法。后面会说明。
⑤什么生成答案放文件里,对比文件那都是小儿科了,学学输入输出流就能搞定。我错了,要我读入文本并计算其中的式子......又要搞字符串啊啊啊啊啊!躲不过啊躲不过。不过还好问题不大,扫一遍字符串,把题目序号去掉,把字符串数字转化为Frac再比较答案就可以了。此处项目已基本完工。


流程图如下:(隐去实现细节)

5. 代码说明:
update 2019/10/11: 优化输入输出流、优化空间。
①数据类型Frac(整数内部也用分数表示,同时包括了符号,反正空间管够)
1 class Frac { 2 public: 3 Frac() { 4 numerator = 0; 5 denominator = 1; 6 opChar = ""; //如果op为空,则表示这个不是运算符 7 } 8 //以下重载+-*/运算符 计算时一边约分并没有什么用,全部是素数的话数字一大就会炸, 9 Frac operator + (const Frac& x) { 10 Frac frac; 11 frac.denominator = this->denominator * x.denominator; 12 frac.numerator = this->numerator * x.denominator + x.numerator * this->denominator; 13 return frac; 14 } 15 Frac operator - (const Frac& x) { 16 Frac frac; 17 frac.denominator = this->denominator * x.denominator; 18 frac.numerator = this->numerator * x.denominator - x.numerator * this->denominator; 19 return frac; 20 } 21 Frac operator / (const Frac& x) { 22 Frac frac; 23 frac.denominator = this->denominator * x.numerator; 24 frac.numerator = this->numerator * x.denominator; 25 return frac; 26 } 27 Frac operator * (const Frac& x) { 28 Frac frac; 29 frac.denominator = this->denominator * x.denominator; 30 frac.numerator = this->numerator * x.numerator; 31 return frac; 32 } 33 //以下是随机生成的函数,或分数或整数或符号 34 void setFraction(int range) { 35 this->denominator = rand() % range + 1; 36 this->numerator = rand() % (range + 1); 37 } 38 void setInteger(int range) { 39 this->numerator = rand() % (range + 1); 40 this->denominator = 1; 41 } 42 void setNum(int range) { 43 this->opChar = ""; 44 rand() % 2 ? setFraction(range) : setInteger(range); 45 } 46 void setOpChar(int isBrac) { 47 if (!isBrac) { 48 int select = rand() % 4; 49 switch (select) { 50 case 0: 51 this->opChar += "+"; 52 break; 53 case 1: 54 this->opChar += "-"; 55 break; 56 case 2: 57 this->opChar += "*"; 58 break; 59 case 3: 60 this->opChar += "÷"; 61 break; 62 default: 63 break; 64 } 65 } 66 else { 67 isBrac == 1 ? this->opChar += "(" : this->opChar += ")"; 68 } 69 } 70 //输出函数,输出分数是最简形式,符号直接输出 71 void print(ofstream &file) { 72 if (this->opChar == "") { 73 int GCD = gcd(this->denominator, this->numerator); 74 this->denominator /= GCD, this->numerator /= GCD; 75 if (this->denominator == 1) //整数 76 file << this->numerator << ' '; 77 else if (this->numerator > this->denominator) { //带分数 78 file << this->numerator / this->denominator << '\'' << this->numerator % this->denominator << '/' << this->denominator << ' '; 79 } 80 else //真分数 81 file << this->numerator << '/' << this->denominator << ' '; 82 } 83 else { 84 file << this->opChar << ' '; 85 } 86 } 87 public: 88 int numerator; 89 int denominator; 90 string opChar; 91 };
②生成表达式:
update:
1 class Expression : public Frac { 2 public: 3 struct treeNode { 4 Frac data; 5 treeNode* lchild, * rchild; 6 treeNode() { 7 lchild = NULL; 8 rchild = NULL; 9 } 10 }; 11 public: 12 void createBinaryTree(treeNode*& root, int num, int range) { 13 root = new treeNode(); 14 num--; 15 if (num <= 0) { 16 root->data.setNum(range); 17 return; 18 } 19 20 root->data.setOpChar(NOTBRAC); 21 int left; 22 while (1) { 23 left = rand() % num; 24 if (left & 1) break; 25 } 26 createBinaryTree(root->lchild, left, range); 27 createBinaryTree(root->rchild, num - left, range); 28 } 29 void destroy(treeNode*& p) { 30 if (p) { 31 destroy(p->lchild); 32 destroy(p->rchild); 33 delete p; 34 p = NULL; 35 } 36 } 37 Frac calBinaryTree(treeNode*& p, string& path) { 38 if (!p->lchild && !p->rchild) 39 return p->data; 40 Frac left = calBinaryTree(p->lchild, path); 41 Frac right = calBinaryTree(p->rchild, path); 42 path += to_string(left.numerator) + to_string(left.denominator) + to_string(right.numerator) + to_string(right.denominator); 43 44 if (left.numerator < 0) return left; //处理负数 45 if (right.numerator < 0) return right; 46 47 string op = p->data.opChar; 48 if (op == "+") 49 return left + right; 50 else if (op == "-") 51 return left - right; 52 else if (op == "*") 53 return left * right; 54 else { 55 if (right.numerator == 0) { 56 right.numerator = -1; 57 return right; 58 } 59 return left / right; 60 } 61 } 62 bool shouldPrintBracket(treeNode*& p, bool leftOrRight) { 63 if (!p) return false; 64 treeNode* left = p->lchild; 65 treeNode* right = p->rchild; 66 if (!left || !right) return false; 67 68 Frac fatherData = p->data; 69 Frac leftData = left->data; 70 Frac rightData = right->data; 71 if (leftOrRight == LEFT) { 72 if (fatherData.opChar == "*" || fatherData.opChar == "÷") { 73 if (leftData.opChar == "+" || leftData.opChar == "-") 74 return true; 75 return false; 76 } 77 else 78 return false; 79 } 80 else { 81 if (fatherData.opChar == "*") 82 return (rightData.opChar == "+" || rightData.opChar == "-") ? true : false; 83 if (fatherData.opChar == "÷") 84 return (rightData.opChar == "+" || rightData.opChar == "-" || rightData.opChar == "*" || rightData.opChar == "÷") ? true : false; 85 if (fatherData.opChar == "+") 86 return (rightData.opChar == "+" || rightData.opChar == "-") ? true : false; 87 if (fatherData.opChar == "-") 88 return (rightData.opChar == "+" || rightData.opChar == "-") ? true : false; 89 return false; 90 } 91 } 92 void recursionPrint(treeNode*& p, ofstream &exerciseFile) { 93 if (!p) return; 94 Frac tmp; 95 if (shouldPrintBracket(p, LEFT)) { 96 exerciseFile << "( "; 97 recursionPrint(p->lchild, exerciseFile); 98 exerciseFile << ") "; 99 } 100 else 101 recursionPrint(p->lchild, exerciseFile); 102 103 p->data.print(exerciseFile); 104 if (shouldPrintBracket(p, RIGHT)) { 105 exerciseFile << "( "; 106 recursionPrint(p->rchild, exerciseFile); 107 exerciseFile << ") "; 108 } 109 else 110 recursionPrint(p->rchild, exerciseFile); 111 } 112 };
③生成/计算表达式/检测文件对答案:
update:
1 class Ans :public Expression { 2 private: 3 vector<Frac> ans; 4 map<string, bool> path; 5 stack<Frac> op, num; 6 vector<Frac> sufExp, beChecked; 7 public: 8 void init() { 9 while (!op.empty()) op.pop(); 10 while (!num.empty()) num.pop(); 11 sufExp.clear(); beChecked.clear(); 12 } 13 //通过判断运算符优先级看是否出栈,其中'('要碰到')'才能出栈,而且不作为后缀表达式一部分,要特殊处理一下 14 bool isPop(string& nowOp, string& stackTopOp) { 15 if (stackTopOp == "(") 16 return false; 17 else if ((nowOp == "*" || nowOp == "÷") && (stackTopOp == "*" || stackTopOp == "÷")) 18 return true; 19 else 20 return (nowOp == "+" || nowOp == "-" || nowOp == ")") ? true : false; 21 } 22 //中缀表达式转化为后缀表达式,exp是中缀,op是运算符栈,sufExp是后缀表达式,具体算法过程参考数据结构书,在此不表 23 void transform(vector<Frac>& exp, stack<Frac>& op, vector<Frac>& sufExp) { 24 for (auto nowFrac : exp) { 25 if (nowFrac.opChar == "") 26 sufExp.push_back(nowFrac); 27 else { 28 if (op.empty()) 29 op.push(nowFrac); 30 else { 31 while (!op.empty() && isPop(nowFrac.opChar, op.top().opChar)) { 32 if (op.top().opChar != "(") 33 sufExp.push_back(op.top()); 34 op.pop(); 35 } 36 if (nowFrac.opChar == ")" && op.top().opChar == "(") 37 op.pop(); 38 if (nowFrac.opChar != ")") 39 op.push(nowFrac); 40 } 41 } 42 } 43 while (!op.empty()) { 44 sufExp.push_back(op.top()); 45 op.pop(); 46 } 47 } 48 //后缀表达式计算,num是数字栈 49 bool calExp(vector<Frac>& sufExp, stack<Frac>& num, bool isChecked) { 50 //cout << sufExp.size() << ' ' << num.size() << endl; 51 Frac num1, num2, num3; 52 string nowpath; 53 for (auto nowFrac : sufExp) { 54 if (nowFrac.opChar == "") 55 num.push(nowFrac); 56 else { 57 num2 = num.top(); num.pop(); 58 num1 = num.top(); num.pop(); 59 if (nowFrac.opChar == "+") 60 num3 = num1 + num2; 61 else if (nowFrac.opChar == "-") 62 num3 = num1 - num2; 63 else if (nowFrac.opChar == "*") 64 num3 = num1 * num2; 65 else { 66 if (num2.numerator == 0) 67 num3.numerator = -1; 68 else 69 num3 = num1 / num2; 70 } 71 72 if (num3.numerator < 0) 73 return false; 74 75 num.push(num3); 76 nowpath += to_string(num3.numerator) + to_string(num3.denominator); //构造运算路径 77 } 78 } 79 //如果nowpath已经在map中标记过,说明生成了效果一模一样的表达式,应当舍弃,重新生成 80 if (isChecked) { 81 if (path[nowpath]) //注意,检验的时候是不能用上map的!!! 82 return false; 83 84 path[nowpath] = true; 85 } 86 ans.push_back(num3); 87 88 return true; 89 } 90 //随机生成表达式的函数 91 void generate(int val, int range) { 92 ofstream exerciseFile, answerFile; 93 exerciseFile.open("Exercises.txt"); 94 answerFile.open("Answers.txt"); 95 /* 96 if (exerciseFile.is_open()) 97 cout << "GOOD" << endl; 98 else 99 cout << "NAH" << endl; 100 */ 101 for (int i = 0; i < val; ++i) { 102 Expression tmp; 103 int mode = rand() % 3; 104 //cout << mode << endl; 105 treeNode* root; 106 //tmp.createBinaryTree(root, 2 * mode + 1, range); 107 if (mode == 0) 108 tmp.createBinaryTree(root, 3, range); 109 else if (mode == 1) 110 tmp.createBinaryTree(root, 5, range); 111 else 112 tmp.createBinaryTree(root, 7, range); 113 114 string pathOfExp = ""; 115 Frac thisAns = tmp.calBinaryTree(root, pathOfExp); 116 pathOfExp += to_string(thisAns.numerator) + to_string(thisAns.denominator); 117 if (thisAns.numerator >= 0 && !path[pathOfExp]) { 118 //cout << "baba" << endl; 119 path[pathOfExp] = true; 120 exerciseFile << i + 1 << ". "; 121 tmp.recursionPrint(root, exerciseFile); 122 exerciseFile << endl; 123 124 answerFile << i + 1 << ". "; 125 thisAns.print(answerFile); 126 answerFile << endl; 127 } 128 else 129 i--; 130 131 destroy(root); 132 init(); 133 } 134 exerciseFile.close(); 135 answerFile.close(); 136 } 137 //字符串转Frac 138 Frac stringToFrac(string& number) { 139 vector <int> part; 140 string tmp = ""; 141 Frac now; 142 number += "$"; //方便处理数字而已,可以忽略,细节罢了 143 144 //string to int 145 for (auto ch : number) { 146 if (isalnum(ch)) tmp += ch; 147 else { 148 if (tmp != "") { 149 part.push_back(stoi(tmp)); 150 tmp = ""; 151 } 152 } 153 } 154 if (part.size() == 1) { //整数 155 now.numerator = part[0]; 156 return now; 157 } 158 else if (part.size() == 2) { //分数 159 now.numerator = part[0], now.denominator = part[1]; 160 return now; 161 } 162 else { //带分数 163 now.numerator = part[0] * part[2] + part[1], now.denominator = part[2]; 164 return now; 165 } 166 } 167 void readAndCheck(string& exercise, string& answer) { 168 ifstream exerciseFile; 169 exerciseFile.open(exercise); //全部用C++的输入输出流 170 string buffer; 171 while (!exerciseFile.eof()) { 172 getline(exerciseFile, buffer); 173 if (buffer.size() == 0) //空行跳过 174 continue; 175 176 string number = ""; 177 buffer += "$"; //加这个无意义符号是为了最后一个数能在循环内被处理掉,用空格也行的 178 179 //把序号去掉 180 for (int i = 0; i < buffer.length(); ++i) { 181 if (buffer[i] == '.') { 182 buffer.erase(0, i + 1); 183 break; 184 } 185 } 186 bool flagOfDiv = false; 187 for (auto ch : buffer) { 188 if (flagOfDiv) { 189 flagOfDiv = false; 190 continue; 191 } 192 //-95对应÷的第一个byte(-95 / -62),拓展ASC码(在string占两位),要特殊处理 193 if (ch == -95) { 194 flagOfDiv = true; 195 if (number != "") { 196 beChecked.push_back(stringToFrac(number)); 197 number = ""; 198 } 199 Frac tmp; 200 tmp.opChar += "÷"; 201 beChecked.push_back(tmp); 202 continue; 203 } 204 205 if (isalnum(ch) || ch == '/' || ch == '\'') 206 number += ch; 207 else { 208 if (number != "") { 209 beChecked.push_back(stringToFrac(number)); 210 number = ""; 211 } 212 if (ch != ' ' && ch != '$') { 213 Frac tmp; 214 tmp.opChar += ch; 215 beChecked.push_back(tmp); 216 } 217 } 218 } 219 220 transform(beChecked, op, sufExp); 221 calExp(sufExp, num, DONTCHECK); //答案将在ans的Frac容器里 222 init(); 223 } 224 exerciseFile.close(); 225 226 //处理用户的答案文件,将答案转化为Frac类型再比较标准答案 227 fstream ansFile; 228 ansFile.open(answer); 229 vector<Frac> userAns; 230 vector<int> correct, wrong; 231 while (!ansFile.eof()) { 232 getline(ansFile, buffer); 233 if (buffer.size() == 0) continue; 234 235 for (int i = 0; i < buffer.length(); ++i) { 236 if (buffer[i] == '.') { 237 buffer.erase(0, i + 1); 238 break; 239 } 240 } 241 userAns.push_back(stringToFrac(buffer)); 242 } 243 ansFile.close(); 244 245 //cout << ans.size() << ' ' << userAns.size() << endl; 246 //对答案 247 Frac tmp; 248 for (int i = 0; i < ans.size(); ++i) { 249 tmp = ans[i] - userAns[i]; 250 if (tmp.numerator == 0) 251 correct.push_back(i + 1); 252 else 253 wrong.push_back(i + 1); 254 } 255 256 //写入成绩 257 ofstream gradeFile; 258 gradeFile.open("Grade.txt"); 259 gradeFile << "Correct: " << correct.size() << " ( "; 260 for (int i = 0; i < correct.size(); ++i) { 261 gradeFile << correct[i]; 262 if (i != correct.size() - 1) 263 gradeFile << ","; 264 } 265 gradeFile << " ) \n"; 266 267 gradeFile << "Wrong: " << wrong.size() << " ( "; 268 for (int i = 0; i < wrong.size(); ++i) { 269 gradeFile << wrong[i]; 270 if (i != wrong.size() - 1) 271 gradeFile << ","; 272 } 273 gradeFile << " ) \n"; 274 gradeFile.close(); 275 } 276 };
6. 测试运行:
①代码覆盖率:
1. 只生成运算式和答案

2. 检查待定题目和答案,不生成式子

②功能检查:
a. 生成式子


b. 生成答案(验过都是对的,因为范围定的是50所以得出答案比较奇怪)

c. 检验用户给出的表达式文件和用户答案正确率(在生成答案的基础上改掉21道题的答案再检验,两种算法相互检验——二叉树生成的表达式用栈检验)


![]()
没什么问题。
7. 总结:只有查重的功能比较不满意,仔细想了一下这个不是很好实现的,例如2 + 3 + 3 和 3 + 2 + 3 和 1 + 4 + 3,不能单纯记录表达式的数字和中间答案判断是否一样,我有一个想法,可惜地方太小写不下。其余功能完美完成,如果需要添加其他运算,修改一下二叉树打括号函数和出栈优先级就行,容易拓展。另外如果要表达式更长,只需要改掉二叉树规模就可以,不用rand模拟表达式出来,用二叉树的好处是能覆盖掉所有括号的情况, 要多长就多长。
额外功能:(来自比赛中做的一道题的部分代码,改一改就能用,保证逻辑正确,无需正则表达式)
1 bool check(string& exp) { 2 int brac = 0; 3 bool num = false; 4 char lastch = ' '; 5 for (auto ch : exp) { 6 if (lastch == '(' && ch == ')') return false; 7 if (lastch == '(' && !isalpha(ch) && ch != '(') return false; 8 if (ch == ')' && !isalpha(lastch) && lastch != ')') return false; 9 lastch = ch; 10 if (ch != '(' && ch != ')') { 11 if (isalnum(ch) && !num) 12 num = true; 13 else if ((ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '%') && num) 14 num = false; 15 else 16 return false; 17 } 18 else { 19 if (ch == '(') 20 ++brac; 21 else if (ch == ')') 22 --brac; 23 if (brac < 0) return false; 24 } 25 26 } 27 if (brac > 0 || !num) 28 return false; 29 30 return true; 31 }
浙公网安备 33010602011771号