结对成员:宗义澎、闫浩宇
一、GitHub地址
https://github.com/amazingyp/QustionHomework
二、PSP:
1 |
Personal Software Process Stages |
预估耗时(分钟) |
实际耗时(分钟) |
·Planning |
·计划 |
30 |
40 |
· Estimate |
· 估计这个任务需要多少时间 |
30 |
20 |
·Development |
·开发 |
300 |
420 |
· Analysis |
· 需求分析 |
60 |
50 |
· Design Spec |
· 生成设计文档 |
30 |
20 |
· Design Review |
· 设计复审 |
30 |
30 |
· Coding Standard |
· 代码规范 |
100 |
120 |
· Design |
· 具体设计 |
60 |
20 |
· Coding |
· 具体编码 |
1200 |
700 |
· Code Review |
· 代码复审 |
20 |
20 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
200 |
100 |
·Reporting |
·报告 |
100 |
140 |
· Test Report |
· 测试报告 |
60 |
50 |
· Size Measurement |
· 计算工作量 |
20 |
20 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
40 |
50 |
合计 |
2010 |
1800 |
三、效能分析
相较于各路大神的各种神仙速度,这个速度实在是慢(似乎是别人的100倍之多?),究其原因,我认为有以下几点:
1.用了过多的随机数:为了实现题目的随机,我在生成题目的许多地方都用了随机数,比如算式中有几个运算符,每个运算符是什么,括号有几个,在第几个位置。这些随机数会降低程序的运行速度
2.数据结构:为了处理方便,我将题目中所有的数都封装成了自己写的分数类(一个分子一个分母),在计算答案时要先封装成分数类在去计算,这也会降低程序的运算速度。
四、设计实现过程
首先阅读需求分析,可以大致将问题分为两部分,一部分是生成题目,另一部分是验证题目正确性。为了让程序的数据结构清晰,我将程序中的所有数据封装成了一个类,然后给出分数类的计算方法。生成题目和验证题目的内容放在了Util包里,具体情况如下图
其中Main是主类,用于处理各种给定的参数;bean包内是分数类;util包中,FileUtil用于处理文件,Number解决如何计算分数类,Question用于生成随机程序
五、代码和具体思路
这部分分为三部分来说
1.生成题目
题目要求给定一个数作为数字的上限,然后生成不能重复的题目,题目的运算符不能超过三个,最后的结果不能是负数。由于题目要求不能重复,我开始的想法是不用随机数,用顺序的方法生成题目,但这样根本不可行,因为题目的范围太大,生成的题目非常相似,不能拿给小学生做,于是还是采用了随机数的策略。但是用了随机数可能就会出现重复的题目,于是我干脆就找它的充分条件:只要答案重复,就认为两道题重复,这样做的合理性是题目范围非常大,生成重复题目概率本就很小,就不要为它花费太多的时间了。另外还需要解决的是运算符和括号的问题,这个地方上面也提到过,用的是随机数,生成随机数决定题目的类型。至于括号,用的则是穷举法,列举出每种情况可能出现的情况,然后再用随机数决定题目属于哪种情况。这个部分的代码主要都是随机数的调用,代码从略。
2.生成答案
这应该是这个项目最难的地方,给定了一个题目,如何算出它的答案呢?我们知道,题目中有括号,另外还要考虑乘除号比加减号运算等级高,所以不能简单地从左到右解析题目,这个地方的算法用的是之前数据结构学过的一套东西,具体算法为现将给出的中缀表达式题目转化为后缀表达式,然后借助栈来运算后缀表达式。具体到这个项目上,在实现算法的时候,需要把字符串形式的题目先转化为中缀表达式,再转化为后缀表达式,与此同时,题目中的数要封装为分数类,便于进行入栈出栈操作,下面代码显示如何实现这个算法。
/** * 将字符串形式的题目转化为Queue<Object>形式的中缀表达式 * */ public static Queue<Object> ReadString(String question) { Queue<Object> mid = new LinkedList<Object>(); int son = -1; String temp = ""; for(int x = 0; x<question.length();x++) { char c = question.charAt(x); if(c=='(') {//左括号前面要么没有,要么就有一个运算符,所以直接放进队列 mid.add(c); } else if(c==')' || c=='+' || c=='-' || c=='×' || c=='÷') { if(son!=-1) {//如果有分子,则说明现在的temp是分母 mid.add(new Num(son,Integer.valueOf(temp))); temp = ""; son = -1; } else if(!temp.equals("")) {//若队列内有字符,则temp是分子,分母为1 mid.add(new Num(Integer.valueOf(temp),1)); temp = ""; } mid.add(c); } else {//读到数字或者分母号 if(c!='/') {//不是分数号,放到temp队列里 temp = temp+c; } else {//遇到分数号,队列的东西提出来做分子 son = Integer.valueOf(temp); temp = ""; } } } //把最后一个数搞出来 if(son!=-1) {//如果有分子,则说明现在的temp是分母 mid.add(new Num(son,Integer.valueOf(temp))); temp = ""; } else if(!temp.equals("")) {//若队列内有字符,则temp是分子,分母为1 mid.add(new Num(Integer.valueOf(temp),1)); temp = ""; } return mid; } /** * 将Queue<Object>计算为最后结果 * 应输入中缀表达式 */ public static Num CountQueue(Queue mid) { Queue<Object> after = MidToAfter(mid); Stack<Object> tempStack = new Stack<Object>(); while(after.peek()!=null) { if(after.peek() instanceof Num) {//操作数直接入栈 tempStack.push(after.poll()); } else { char op = (char)after.poll(); Num n,first,second; switch (op) { case '+': n = NumberUtil.add((Num)tempStack.pop(),(Num)tempStack.pop()); tempStack.push(n); break; case '-': first = (Num)tempStack.pop(); second = (Num)tempStack.pop(); n = NumberUtil.sub(second,first); tempStack.push(n); break; case '×': n = NumberUtil.mul((Num)tempStack.pop(),(Num)tempStack.pop()); tempStack.push(n); break; case '÷': first = (Num)tempStack.pop(); second = (Num)tempStack.pop(); n = NumberUtil.div(second,first); tempStack.push(n); break; } } } Num answer = (Num)tempStack.pop(); return NumberUtil.normal(answer); } /** * 将中缀表达式转换为后缀表达式 * */ public static Queue<Object> MidToAfter(Queue mid) { Queue<Object> after = new LinkedList<Object>(); Stack<Object> tempStack = new Stack<Object>(); while(mid.peek()!=null) { if(mid.peek() instanceof Num) {//操作数直接入队列 after.add(mid.poll()); } else {//符号的判定 char c = (char) mid.poll(); if(c=='(') {//左括号直接入栈 tempStack.add(c); } else if(c==')') {//右括号把栈里的东西全弄到队列里,直到遇到左括号 while (true) { if (tempStack.empty()) { System.out.println("缺少左括号! "); return null; } else if ((char)tempStack.peek()=='(') { tempStack.pop(); break; } else { after.add(tempStack.pop()); } } } //非括号类运算符 else if (!tempStack.empty()) { char peek = (char)tempStack.peek(); //当前运算符优先级大于栈顶运算符优先级,或者栈顶为左括号时,当前运算符直接入栈 if(((c=='×' ||c=='÷')&&((peek=='+') || (peek=='-'))) || peek=='(') { tempStack.push(c); } //否则,将栈顶的运算符取出并存入队列,然后将自己入栈 else { after.add(tempStack.pop()); tempStack.push(c); } } else { tempStack.push(c); } } } while(!tempStack.empty()) { after.add(tempStack.pop()); } return after; }
以上的三个方法可以实现上述算法。
3.检查用户输入
这个部分是由队友完成,我直接把他的实现方法贴上来:
1)打开习题册,创建一个文件选择器,由用户自己选择要练习的习题册。将选择的文件逐行读取,并显示在控制台上。前提是已经有生成过的题目文件在储存盘中。
2)输入答案。由用户自行根据习题册内容输入自己的答案,并以换行为题目分割。同样用文件选择器保存到目标路径下。
3)检查。分别将答案文件与用户用io流读取。逐行比较,并记录错题数与题号。将保存好的错题记录输出。
六、测试运行
输入生成题目的指令:
在项目的questionbank中生成了对应文件
题目文件的具体内容:
输入检查的指令
输入指令后成功验证答案
七、项目小结
通过这个项目,体会到了结对编程的好处,结对编程可以动用两个人的智慧,想到更多的好方法,除此之外,通过这个项目,对之前的Java知识也有了更深的认识。这个项目完成的不好的地方是没有过多的考虑程序运行时间的问题,导致了程序效能极低,以后需要在这方面多下功夫。