2019/10/10 update:式子生成部分已经用二叉树重写掉,简洁好用,能覆盖所有可能的括号位置。

1. github地址:https://github.com/CourierLo/ExercisesForPupils

2. PSP表格:

PSP2.1

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 };
Frac

②生成表达式:

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 };
Stack

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 }
判断用户的式子是否合法