任务:实现一个自动生成小学四则运算题目并具有题目答案修改功能的程序。(http://www.cnblogs.com/jiel/p/4810756.html)
各模块预计和实际耗费时间
PSP2.1 |
Personal Software Process Stages |
Time |
Planning |
计划 |
|
· Estimate |
· 估计这个任务需要多少时间 |
24h |
Development |
开发 |
|
· Analysis |
· 需求分析 (包括学习新技术) |
30min |
· Design Spec |
· 生成设计文档 |
1h |
· Design Review |
· 设计复审 (和同事审核设计文档) |
30min |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
1h |
· Design |
· 具体设计 |
3h |
· Coding |
· 具体编码 |
7h |
· Code Review |
· 代码复审 |
1h |
· Test |
· 测试(自我测试,修改代码,提交修改) |
5h |
Reporting |
报告 |
|
· Test Report |
· 测试报告 |
1h |
· Size Measurement |
· 计算工作量 |
10min |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
30min |
合计 |
20h40min |
过程
经过对程序要求的分析,我进行了一个大致的规划,设计了Number类、Expression类,Number用于创建数的对象,包括自然数、真分数和带分数,Expression类用于创建子表达式,并存储表达式。使用C++来编写。
Number类设置了三个属性,能同时用来创建自然数,真分数和假分数。Expression类利用递归的方法存放带括号的完整表达式。
先随机生成表达式包含的数字个数,然后随机生成不同的数字,然后依旧是随机选取数字来完成括号的生成。例:1, 2/3, 1'3/4, 2三个数字和×, +, -三个个运算符号,随机选几个数字来生成括号,例:选择1, 2/3和×,则得到(1 × 2/3) + 1'3/4 - 2
不过,在实现第二个主要的功能时需要对字符串进行解析,而使用C++的话复杂度会很大,因为C++在string方面没有像JAVA、C#有split之类的函数,而这些功能函数在C++还要自己再写一遍,于是思前想后果断把代码移植到C#上去了。
遇到另一个比较棘手的问题就是对于符号‘×’ ‘÷’的输出和读取,在没有换C#之前,我也找了很多关于C++输出Unicode字符的方法,但是并不能达到预期的效果,渐渐的也就对C++失去的信心。把代码移植到C#上之后,这个问题出乎意料的迎刃而解了,而且不用费很大的功夫,只用在读文件的时候设置文件的编码格式再读,输出时直接就可以输出了。
通过上面两个问题的解决,真心地觉得在写工程之前一定要选择对的语言,不然等碰壁之后再换就少不了麻烦了。
对于性能这块儿,我并没有花太多时间来改进,因为我在写的过程中就尝试争取使用最优化的方法来写,只要我能想到的,我就都会去比较,这也就是为什么我编写的过程中也会耗费相对来说比较长的时间了。
整个工程中对整个程序的性能影响最大的应该是“查重”,因为如果要完全做到两两不相重的话,一一对比应该是最稳妥的方法了,所以如果真的要生成10000个,那么一一对比相对来说真的会拖延整个程序的运行速度。
来一段代码吧,Expression类
class Expression { private const char opr_plus = '+'; private const char opr_sub = '-'; private const char opr_multi = 'u00D7'; private const char opr_devide = 'u00F7'; public List<Expression> expressions = new List<Expression>(); public List<char> operators = new List<char>(); public Number answer = new Number(); public string oprs = ""; // for comparision public string nums = ""; // for comparision public Expression() { } public Expression(List<Expression> expressions, Random rand) { this.expressions = expressions; int n = expressions.Count(); for (int i = 0; i < n - 1; i++) { int temp = rand.Next(4); switch (temp) { case 0: operators.Add(opr_plus); break; case 1: operators.Add(opr_sub); break; case 2: operators.Add(opr_multi); break; case 3: operators.Add(opr_devide); break; } } } public Expression(String e) { if (!e.Contains(' ')) answer = new Number(e); // 1'2/3 else if (!e.Contains('(') && !e.Contains(')')) // 2/3 + 1 - 3/4 × 1 { string[] ss = e.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < ss.Count() / 2; i++) { expressions.Add(new Expression(ss[2 * i])); operators.Add(ss[2 * i + 1].ElementAt(0)); } expressions.Add(new Expression(ss.Last())); } else { while (e.Contains(' ')) { if (e.First() != '(') { int pos = e.IndexOf(' '); expressions.Add(new Expression(e.Substring(0, pos))); e = e.Substring(pos + 1); } else { int flag = 1; for (int i = 1; i < e.Count(); i++) { if (e.ElementAt(i) == ')') flag--; else if (e.ElementAt(i) == '(') flag++; if (flag == 0) { flag = i; break; } } expressions.Add(new Expression(e.Substring(1, flag - 1))); if (flag == e.Count() - 1) e = ""; else e = e.Substring(flag + 2); } if (e == "") break; operators.Add(e.First()); e = e.Substring(2); } if (e != "") expressions.Add(new Expression(e)); } } public Expression(Expression other) { expressions.AddRange(other.expressions); operators.AddRange(other.operators); answer = new Number(other.answer); } public Boolean calculate() { if (expressions.Count() == 0) return true; List<Expression> exp = new List<Expression>(); List<char> opr = new List<char>(); for (int i = 0; i < expressions.Count(); i++) if (expressions[i].expressions.Count() != 0) if (!expressions[i].calculate()) return false; for (int i = 0; i < expressions.Count(); i++) exp.Add(new Expression(expressions[i])); for (int i = 0; i < operators.Count(); i++) opr.Add(operators[i]); for (int i = 0; i < opr.Count(); i++) { if (opr[i] != opr_multi && opr[i] != opr_devide) continue; if (opr[i] == opr_multi) exp[i].answer.multi(exp[i + 1].answer); if (opr[i] == opr_devide) { if (exp[i + 1].answer.isZero()) return false; // devide 0 error! exp[i].answer.devide(exp[i + 1].answer); } exp.RemoveAt(i + 1); opr.RemoveAt(i); i--; } while (opr.Count() != 0) { if (opr[0] == opr_plus) exp[0].answer.plus(exp[0 + 1].answer); if (opr[0] == opr_sub) exp[0].answer.sub(exp[0 + 1].answer); if (exp[0].answer.isNegative()) return false; // a - b < 0 error! exp.RemoveAt(1); opr.RemoveAt(0); } answer = exp[0].answer; return true; } public Boolean isSimilar(Expression other) { if (!answer.isEqual(other.answer)) return false; if (other.oprs == "") other.oprsString(); oprsString(); if (!oprs.Equals(other.oprs)) return false; if (other.nums == "") other.numsString(); numsString(); if (!nums.Equals(other.nums)) return false; return true; } public String toString() { if (expressions.Count() == 0) return answer.toString(); String s = ""; int count = expressions.Count(); for (int i = 0; i < count - 1; i++) s = s + expressions[i].toString() + " " + operators[i] + " "; s = "(" + s + expressions[count - 1].toString() + ")"; return s; } private List<char> getOprs() { List<char> list = new List<char>(); list.AddRange(operators); foreach (Expression e in expressions) if (e.expressions.Count() != 0) list.AddRange(e.getOprs()); return list; } private List<Number> getNums() { List<Number> list = new List<Number>(); foreach (Expression e in expressions) { if (e.expressions.Count() != 0) list.AddRange(e.getNums()); else list.Add(e.answer); } return list; } private void oprsString() { List<char> list = getOprs(); list.Sort(); oprs = new string(list.ToArray()); } private void numsString() { List<Number> list = getNums(); List<string> a = new List<string>(); foreach (Number n in list) a.Add(n.toString()); a.Sort(); nums = string.Join("", a.ToArray()); } }
性能分析图
(由VS2013性能分析工具自动生成)
CPU占用分析图,两个峰值,第一个是 -r 10 -n 10000的指令执行,第二个是 -a a.txt -e e.txt 的指令执行(其中包含10000道题)
可见,查重(函数isSimilar)使用的频率最大
isSimilar包含在run_producing函数中,依旧是isSimilar占很大的使用频率
测试
测试用例:
- -r 100 -n 100
- -r 10 -n 10000
- -r 10 -n 10001
- -n 10 -r 10
- -n 10.2 -r 100
- -n -1 -r 10
- -n 0 -r 10
- -n 10 -r 0
- -r 10.2 -n 10
e.txt
1. 7'5/6 + 2'3/8 ÷ 5'5/6 =
2. 9'4/5 ÷ 7 + 7'2/5 =
3. 1/3 ÷ 8 =
4. (5 × 1/2 ÷ 7'1/3) + 5'4/9 =
5. 9'4/5 ÷ 1'5/7 =
6. 8'1/8 - 3'1/2 =
7. (6 × 4'1/2) ÷ 2'7/9 + 10 =
8. (3'7/9 + 1/2) × 1/3 =
9. 1/5 ÷ 8'8/9 × 6 =
10. 8'5/6 - 1/4 =
a.txt
1. 8'101/420
2. 8'4/5
3. 1/24
4. 5'311/396
5. 5'43/60
6. 4'5/8
7. 19'18/25
8. 1'23/54
9. 27/200
10. 8'7/12
结果:
Correct: 10 (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Wrong: 0
e.txt
1. 7'5/6 + 2'3/8 ÷ 5'5/6 =
2. 9'4/5 ÷ 7 + 7'2/5 =
3. 1/3 ÷ 8 =
4. (5 × 1/2 ÷ 7'1/3) + 5'4/9 =
5. 9'4/5 ÷ 1'5/7 =
a.txt
1. 8'101/42
2. 8'1/5
3. 1/24
4. 5'31/396
5. 5'43/60
结果:
Correct: 2 (3, 5)
Wrong: 3 (1, 2, 4)
很明显,我只更改了1,2,4题的答案,所以最后的judging结果是正确的。
e.txt
1. 1/2 ÷ 2/3 × 6'1/3 + 3/4 =
2. 5 ÷ (3/4 + 5'1/2) × 0 =
3. 6'1/5 ÷ 1 =
4. 7'4/7 - 1/6 - 1/3 × 2/9 =
5. 5'3/7 + 7/9 × 2'1/4 =
a.txt
1. 5'1/2
2. 0
3. 3'1/10
4. 7'125/378
5. 7'89/224
结果:
Correct: 3 (1, 2, 4)
Wrong: 2 (3, 5)
我使用自己生成的10000个表达式进行检查得到的100%的正确结果,然后稍微修改里面的个别,得出的结果也是和修改的记录相符。对于表达式数量较少的,我会自己手动计算,多次测试之后,几乎可以认定是完全正确了。
收获与感想
这项作业从周五开始入手,因为需求太多,所以为了在编程的时候不去再做很多的修改,所以在变成前的设计方面花了很长时间。
先开始是用C++写的,还挺顺利的,因为我的C++用的频率较多,所以得心应手。
但是,在实现第二个功能的时候需要对字符串进行解析,而C++在string方面没有像JAVA、C#有split之类的函数,所以果断把代码移植到C#上去了。
我觉得这一工程,首先让我对C++和C#之间的优缺对比有了大概的认知,提高了我C#的能力。
其次,也是最重要的就是在工程中找到最优的设计,说真的,我一开始的设计很阻碍我的进程,后来还是抽出将近1个小时的时间来设计新的结构,这才使我能顺利完成这项任务。