github地址

一、需求

题目:实现一个自动生成小学四则运算题目的命令行程序

功能均已经实现:

  • 使用 -n 参数控制生成题目的个数
  • 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围
  • 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1 − e2的子表达式,那么e1 ≥ e2
  • 生成的题目中如果存在形如e1 ÷ e2的子表达式,那么其结果应是真分数
  • 每道题目中出现的运算符个数不超过3个
  • 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目
  • 在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt
  • 程序应能支持一万道题目的生成
  • 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,统计结果输出到文件Grade.txt

二、耗费时间估计

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30
· Estimate · 估计这个任务需要多少时间 30
Development 开发 740
· Analysis · 需求分析 (包括学习新技术) 240
· Design Spec · 生成设计文档 120
· Design Review · 设计复审 (和同事审核设计文档) 40
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10
· Design · 具体设计 60
· Coding · 具体编码 30
· Code Review · 代码复审 120
· Test · 测试(自我测试,修改代码,提交修改) 120
Reporting 报告 60
· Test Report · 测试报告 40
· Size Measurement · 计算工作量 10
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 10
合计 1640

三、效能分析

-n 10000 -r 10的分析:

可见生成一万道题目,耗时不到1.1秒,运行时内存消耗为38M,程序运行良好。

我们将题目加大到一百万条,为了减去表达式重复碰撞的影响,我们将r值提高到100,参数为:

-n 1000000 -r 100,结果如下图:

可以看到,这次的运行时间为44秒左右,虽然生成的题目增加了100倍,但是运行时间只增加了40倍左右,运行时内存占用0.54GB,CPU占用为20.43%,当进行GC垃圾回收活动的时候,CPU占用率就会明显升高,可见CPU时间主要消耗在垃圾回收上,这与我们的设计思想有关,我们设计的每个表达式都是一个对象,每个表达式对象里面都有一棵二叉树,每个对象都会占用一部分堆内存,生成一百万道题目就意味着堆内存里面要放一百万个对象,这显然是不可能的,所以需要不断的进行垃圾回收,将使用过的表达式对象所占用的内存回收掉,改进思路是减少表达式对象中不必要的属性,或者将表达式用其他方式而不是一个对象表示,由于时间限制目前还没有进行改进

看下统计这一百万道题目的答案时的运行情况:

总耗时35秒,内存占用120.3MB,CPU占用18.96%,程序运行良好,统计一百万道表达式的答案的速度比生成一百万道表达式快,我们觉得这是因为不需要进行表达式的判重操作,而且表达式对象不需要放在HashSet中,所以用完可以立即进行垃圾回收,占用的内存也减少了。

四、设计实现过程

1.数值的表示

四则运算表达式中要求题目中的数值为自然数或真分数,这里我们设计一个分数类Fraction,将所有数值用分数来表示,这个分数类只有两个属性moleculedenominator,即分子和分母,运算的时候通过分数的分子分母进行运算,所以我们需要一个方法将自然数转化为分数,这个方法是intChangeToFraction(int)。同时,我们还需要一个方法newFraction(int)用来随机生成分数,参数为表达式中数值的最大取值。最后是实现分数的加减乘除操作方法。当需要打印到表达式时,我们重写它的toString()方法,将它用真分数的形式打印出来即可。该类的结构图如下:

2.四则运算表达式的表示

我们平时看到的四则表达式如 a+b+c 是表达式的中缀表示,不方便用于机器计算结果,所以我们使用表达式的后缀表示法(即逆波兰表示法),上面的式子转化逆波兰式为:ab+c+,按照逆波兰表达式的计算方法这个式子可以用二叉树来表示:

通过二叉树,我们可以对四则运算表达式的结果进行计算,实现对四则运算表达式的判重。

(1)二叉树节点对象BinaryTree

二叉树的每个节点有四个属性:符号symbol、分数fraction以及左右子树节点。如果是叶子节点那么它的symbol为空。中序遍历二叉树的根节点,即可打印出中缀表达式的形式,因此我们需要一个中序遍历的方法:midTraversing()。还有需要注意的是,中序遍历的时候有3种情况需要加括号:
1.当前节点的符号是减号,右子树的节点符号是加号或减号时,表达式右边需要加括号,如式子c-(a+b)的二叉树为:

如果不加括号,中序遍历的结果为c-a+b,这样显然不对。我们就是刚开始的时候少考虑了这种情况,忘记加这种情况的括号,导致打印出来的表达式是错误的,和计算结果匹配不上。

2.如果当前节点的符号是乘法,左右子树的节点是加法或减法,左右子树加括号

3.如果当前节点的符号是除法,右子树如果是符号的话都加括号,左子树的符号是加法或减法加括号。

所以这里我们加一个addBrackets()方法判断是否需要加括号。

这个二叉树类的结构如下:

(2)表达式对象Expression

表达式对象的属性有:二叉树对象root、计算结果result、中序遍历二叉树得到的表达式的字符串形式expression。这里我们生成表达式对象有两种情况:
1.-n -r生成题目的时候,随机生成表达式对象,使用构造方法Expression(int)

int 是限制的最大自然数

随机生成表达式,即我们需要随机生成一棵二叉树,实现一个generateBinaryTree(int,int),第一个参数是最大自然数,第二个参数是符号数,给当前节点分配一个随机生成符号,给左子树分配0到(符号数-1)个符号,给右子数分配符号数-1-分配给左子树的符号数个符号;如果符号数为0,则给当前节点随机生成一个分数。

随机生成二叉树后,我们还需要一个getResult(BinaryTree)方法来计算二叉树的节点,这个方法通过左子树的值[+|-|*|÷]右子树的值,遍历得到结果。

2.-e 题目.txt -a answers.txt统计题目和答案的时候,根据表达式的字符串来生成表达式对象,使用构造方法Expression(String)

需要先将中缀表达式转为逆波兰表达式,再将逆波兰表达式生成二叉树,这里用到了2个方法:changeExpressionToReversePoland(String)reversePolandToTree(String)

具体思想如下:

用一个栈来实现
遍历读取输入的四则运算表达式

规则:如果是数字,直接加到逆波兰表达式中
如果是‘(’直接加到栈中
如果是‘)’一直弹出栈里的元素到逆波兰表达式,直到遇到第一个‘(’弹出为止,‘(’不加入逆波兰表达式,‘)’不加入栈中
如果是’+‘或‘-’,一直弹出栈里面的元素直到遇到第一个‘(’就不弹出或者栈空为止,加入栈中。
如果是‘’或‘/’,如果栈为空或者栈顶元素是‘+’或‘-’直接加入栈中;如果栈顶元素是‘’或‘/’弹出到逆波兰表达式后,加入栈中。
读取完毕后,将栈中剩余操作符挨个出栈并加入到后缀表达式中。

至于将逆波兰表达式转为二叉树就很简单了,具体实现在后面的代码中可以看到。

Expression类的结构:

(3)表达式判重

判断的时候如果当前节点的符号和左右子树相同,则是相同的树;否则如果当前节点的符号是+或*,交换左右子树进行比较,如果相同则是相同的数。

比如上面这两棵树,1号节点和2号节点进行比较,因为左右子数不同,交换左右子数比较,3和6比较,相同,4和5比较,左右子数不同,交换左右子树比较,7和10比较,相同,8和9比较,相同。故1和2相同。

具体的实现是将生成的Expression对象放在HashSet中,HashSet的不允许放置重复的对象,每次添加对象,它会判断该的hashcode是否相等,如果相等,再判断调用对象的equals()方法判断是否与已经添加的对象equals,如果equals,则不允许添加,返回false。

(4)负数的处理

如果计算过程中出现负数,只需要交换左右子树的节点即可,如下图,10-15出现负数,我们将10和15交换:

五、代码说明

Fraction.java关键代码:

/*** 将分数对象按真分数的表示方法打印* @return String*/@Overridepublic String toString() {if(denominator==1){return molecule+"";}else{int prefix = molecule/denominator;int realMolecule = molecule % denominator;if(realMolecule==0){return prefix+"";}//拿分子分母的最小公因数int commonFactor = Math.abs(getCommonFactor(denominator,realMolecule));if(prefix==0){return realMolecule/commonFactor+"/"+denominator/commonFactor;}elsereturn prefix+"'"+realMolecule/commonFactor+"/"+denominator/commonFactor;}}

BinaryTree.java关键代码:

/**** 中序遍历* @return*/
public String  midTraversing() {BinaryTree node = this;StringBuilder expression = new StringBuilder();if(node.symbol!=null){String symbol = node.symbol;String left = node.leftChild.midTraversing();String right = node.rightChild.midTraversing();if(addBrackets(symbol,node.leftChild.symbol,1)){expression.append("( ").append(left+" ) ").append(symbol+" ");}else{expression.append(left+" ").append(symbol+" ");}if(addBrackets(symbol,node.rightChild.symbol,2)){expression.append("( ").append(right+ " ) ");}else{expression.append(right);}return expression.toString();}else{return node.fraction.toString();}
}/**** 判断是否需要加括号* @param symbol1* @param symbol2* @param leftOrRight 1表示left,2表示right* @return*/
public static boolean addBrackets(String symbol1,String symbol2,int leftOrRight){if(symbol2==null){return false;}if(symbol1.equals(SYMBOL[1])){if(symbol2.equals(SYMBOL[1])||symbol2.equals(SYMBOL[0])) {if (leftOrRight == 2) return true;}}if(symbol1.equals(SYMBOL[2])){if(symbol2.equals(SYMBOL[0])||symbol2.equals(SYMBOL[1])){return true;}}if(symbol1.equals(SYMBOL[3])){if(symbol2.equals(SYMBOL[0])||symbol2.equals(SYMBOL[1])){return true;}if(leftOrRight==2){return true;}}return false;
}@Override
public boolean equals(Object obj) {if (obj != null) {BinaryTree binaryTree = (BinaryTree) obj;boolean l = false;//判断左子数是否相等boolean r = false;//判断右子数是否相等boolean f;//判断数值是否相等if(binaryTree.fraction!=null){f = binaryTree.fraction.equals(this.fraction);}else{f = this.fraction == null;}boolean s ;//判断符号是否相等if(binaryTree.symbol!=null){s = binaryTree.symbol.equals(this.symbol);}else{s = this.symbol==null;}if (binaryTree.leftChild != null && binaryTree.rightChild != null&&f&&s) {l = binaryTree.leftChild.equals(this.leftChild);r = binaryTree.rightChild.equals(this.rightChild);if(l==false&&r==false&&(binaryTree.symbol.equals(SYMBOL[0])||binaryTree.symbol.equals(SYMBOL[2]))){//左右子数交换比较r = binaryTree.rightChild.equals(this.leftChild);l = binaryTree.leftChild.equals(this.rightChild);}}else{l = this.leftChild==null;r = this.rightChild==null;}return l && r && f && s;} else {return false;}
}

Expression.java关键代码:

/*** 将普通表达式转化为逆波兰表达式* @param expression* @return*/
public String changExpressionToReversePoland(String expression){//逆波兰表达式StringBuilder profixExpr = new StringBuilder();String[] elements = expression.split(" ");Stack<String> stack = new Stack<String>();String element = null;String pop = null;for(int i=0; i<elements.length; i++){element = elements[i];//如果当前字符为操作数if(!isSymbol(element)&&!element.equals("(")&&!element.equals(")")) {profixExpr.append(element+" ");}//如果当前字符为操作符else if(isSymbol(element)) {if(stack.isEmpty())stack.push(element);else {while(true) {if(stack.isEmpty() || priority(stack.peek()) < priority(element))break;pop = stack.pop();profixExpr.append(pop+" ");}stack.push(element);}}//如果当前字符为‘(’else if("(" .equals(element) ) {stack.push(element);}//如果当前字符为‘)’else if(")".equals(element)) {while(!(pop = stack.pop() ).equals("("))profixExpr.append(pop+" ");}elseSystem.out.println("输入文件中表达式有错误");}while(!stack.isEmpty()){profixExpr.append(stack.pop()+" ");}String profixExprTem = profixExpr.toString();return profixExprTem.substring(0,profixExprTem.length()-1);//去掉最后的" "
}/*** 由逆波兰表达式生成二叉树* @param elements* @param node*/
public void reversePolandToTree(String[] elements,BinaryTree node){if(isSymbol(elements[theLengthOfprofixExpr])){//拿逆波兰表达式的最后一位生成当前节点node.symbol = elements[theLengthOfprofixExpr];theLengthOfprofixExpr--;//先生成右子树,再生成左子数node.rightChild = new BinaryTree();reversePolandToTree(elements,node.rightChild);node.leftChild = new BinaryTree();reversePolandToTree(elements,node.leftChild);}else{node.fraction = new Fraction(elements[theLengthOfprofixExpr]);theLengthOfprofixExpr--;return ;}
}/*** 随机生成二叉树* @param maxNum* @param symbolNum* @return*/
public BinaryTree generateBinaryTree(int maxNum, int symbolNum){BinaryTree binaryTree = new BinaryTree();if(symbolNum==0){binaryTree.fraction = NumberFactory.getNumber(maxNum);}else{binaryTree.symbol = SYMBOL[(int)(Math.random() * 4)];int leaveSymbolNum = symbolNum-1;int symbolNumToLeft = (int)(Math.random()*(leaveSymbolNum+1));binaryTree.leftChild = generateBinaryTree(maxNum,symbolNumToLeft);binaryTree.rightChild = generateBinaryTree(maxNum,leaveSymbolNum-symbolNumToLeft);}return binaryTree;
}/*** 计算二叉树的结果* @param binaryTree* @return* @throws Exception*/
public Fraction getResult(BinaryTree binaryTree) throws Exception {if(binaryTree.leftChild==null&&binaryTree.rightChild==null){return binaryTree.fraction;}else{String symbol = binaryTree.symbol;Fraction leftChildFraction = getResult(binaryTree.leftChild);Fraction rightChildFraction = getResult(binaryTree.rightChild);binaryTree.fraction = operation(symbol,leftChildFraction,rightChildFraction);//若结果为负数,左右子树交换,值取绝对值if(binaryTree.fraction.getMolecule()<0){binaryTree.fraction.setMolecule(Math.abs(binaryTree.fraction.getMolecule()));BinaryTree node = binaryTree.leftChild;binaryTree.leftChild = binaryTree.rightChild;binaryTree.rightChild = node;}return binaryTree.fraction;}
}

六、测试运行

保证程序正确的关键:单元测试,这次项目,我们对每个方法都进行了单元测试,确保每个方法都没有错误才继续写下一个方法。

生成的一万道题目:

答案:

统计结果:

七、PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 60
· Estimate · 估计这个任务需要多少时间 30 60
Development 开发 1040 2040
· Analysis · 需求分析 (包括学习新技术) 240 480
· Design Spec · 生成设计文档 120 240
· Design Review · 设计复审 (和同事审核设计文档) 40 40
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10 10
· Design · 具体设计 60 120
· Coding · 具体编码 330 630
· Code Review · 代码复审 120 240
· Test · 测试(自我测试,修改代码,提交修改) 120 120
Reporting 报告 60 60
· Test Report · 测试报告 40 40
· Size Measurement · 计算工作量 10 10
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 10 10
合计 1230 2160

八、项目小结

第一次尝试采用结对编程(Pair Programming)这种编程模式,虽然时间不长,但还是感觉体会颇多。
本次结对编程我俩是舍友。本次结对编程的总体体验是非常愉快的,编程时及时得到同伴的肯定,自信心和成就感会增强,这样就会提高生产率。在开始定方案的时候,两个人分别找资料找思路,方案选出了彼此认为最优秀的一个,二叉树。

结对编程之初,我们两个的配合还是有些不顺畅,编码习惯有差异,甚至对数值的定义起名方式存在差异,会影响到我们的效率。在编写代码的过程中,相互督促,可以使我们都能集中精力,更加认真的工作,不间断的Code Review,提高代码质量。任何一段代码都至少被两双眼睛看过,两个脑袋思考过,大家的思维互相补充,许多隐藏的bug当场就被提出,被消灭在萌芽之中,代码的质量会得到有效提高。

两个人一起编程难免出现意见不一致的现象,出现这种情况我们采取的方式是停止手头的工作,直到讨论清楚得出结论为止,有时候我们这样的讨论可能持续时间比较长,会影响到我们的生产力。很多代码优化的步骤在编写代码的时候就被提出并且改进,我认为这非常重要。整个代码是两个人一起完成的,每个人都非常熟悉整个代码,这非常方便后面的debug调试。

编程耗时最多的方面就是debug。在我们得出设计思路,并将它们初次转化成代码后,往往会有bug。
在设计代码时,有个同伴可以一起讨论,融合两个人不同的见解和观点,我们往往可以得出更加准确且更加高效的设计思路。

转载于:https://www.cnblogs.com/guohanghuang/p/9723038.html

四则运算题目 Java实现 by 黄国航 黄宇航相关推荐

  1. java实现加减乘除运算符随机生成十道题并判断对错_简单小程序——产生三十道小学四则运算题目...

    题目要求程序可以生成三十道小学四则运算题目. 因为要随机生成题目,则需要产生随机数,因此我上网搜索了生成随机数的方法,选择了使用Random类得到规定范围内的随机数.因为一个运算需要三个元素,两个参与 ...

  2. 【2017下集美大学软工1412班_助教博客】个人作业1——四则运算题目生成程序 成绩公示...

    作业要求 个人作业1--四则运算题目生成程序(基于控制台) 使用 -n 参数控制生成题目的个数 使用 -r 参数控制题目中数值 生成的题目中如果存在形如e1 ÷ e2的子表达式,那么其结果应是真分数 ...

  3. java生成四则运算表达式_生成四则运算(java实现)

    |博客班级 | https://edu.cnblogs.com/campus/ahgc/AHPU-SE-19/ | |作业要求 | https://edu.cnblogs.com/campus/ahg ...

  4. 高级软件工程2017第2次作业—— 个人项目:四则运算题目生成程序(基于控制台)...

    Deadline:2017-09-27(周三) 21:00pm (注:以下内容参考 福大软工作业 和集大个人作业 ) 0.前言 很多童鞋在本课程的目标和规划中,都表示希望能提高自己的实践能力. Pra ...

  5. 结对编程—四则运算(JAVA)(卢泰佑、李密)

    Github项目链接:https://github.com/lutys/arithmetic 一.项目简介 项目要求实现一个自动生成小学四则运算题目的命令行程序. 自然数:0, 1, 2, -. 真分 ...

  6. postfixcalc函数 java_结对编程--四则运算(Java)萧英杰 夏浚杰

    结对编程--四则运算(Java)萧英杰 夏浚杰 功能要求 题目:实现一个自动生成小学四则运算题目的命令行程序 使用 -n 参数控制生成题目的个数(实现) 使用 -r 参数控制题目中数值(自然数.真分数 ...

  7. 四则运算游戏 java代码_四则运算程序(java基于控制台)

    一.题目描述: 1. 使用 -n 参数控制生成题目的个数,例如 Myapp.exe -n 10 -o Exercise.txt 将生成10个题目. 2. 使用 -r 参数控制题目中数值(自然数.真分数 ...

  8. myapp——自动生成小学四则运算题目的命令行程序(侯国鑫 谢嘉帆)

    1.Github项目地址 https://github.com/baiyexing/myapp.git 2.功能要求 题目:实现一个自动生成小学四则运算题目的命令行程序 功能(已全部实现) 使用 -n ...

  9. 个人作业1 四则运算题目生成程序

    项目地址:https://gitee.com/wenguixin/javascript_four_algorithms.git 1.题目描述: 生成定量小学四则运算的题目. 2.需求分析: 在现今的时 ...

最新文章

  1. Key-Value数据库:Redis与Memcached之间如何选择?
  2. Python爬取B站5000条视频,揭秘为何千万人为它流泪
  3. linux init进程是所有用户进程的祖先进程,Linux中init进程介绍及常用方法
  4. Linux系统设置全局的默认网络代理
  5. 《数据库技术原理与应用教程(第2版)》——习 题 1
  6. TensorFlow 教程——电影评论文本分类
  7. radio 取值赋值 亲测有用实效
  8. javascript经典实例_一道前端经常忽视的JavaScript面试题
  9. python requests cookie_python requests 带cookie访问页面
  10. Git 操作总结整合篇
  11. Spring Boot学习总结(4)——使用Springloaded进行热部署
  12. javaJavaScript DOM
  13. POJ- 1751 Highways
  14. c语言记账系统源程序,C语言会计记账管理系统.doc
  15. c语言把数字转换为字母,C语言将字符串转数字
  16. 计算机管理五大功能,操作系统五大管理功能包括哪些介绍大全
  17. power apps canvas团队协作开发总结的几种方式
  18. wireshark:包重组
  19. 两种重要的数据【逻辑数据模型,概念数据模型】
  20. 线程池为啥要用阻塞队列

热门文章

  1. Linux串口编程 —— 4G模块短信获取与删除
  2. 引领高并发直播场景进入毫秒时代,阿里云发布超低延时直播服务
  3. java计算机毕业设计智能快递分拣系统源码+mysql数据库+系统+lw文档+部署
  4. Android手机APK功耗、流量、内存测试方法
  5. 一款成功的全球服游戏该如何进行架构选型与设计?
  6. opencv 图像金字塔
  7. 哪种平板电脑适合一级计算机考证
  8. java中三种可能导致异常的情况_JAVA基础知识点之异常
  9. 光环python培训怎么样
  10. 如何测试麦克风和扬声器(耳机)