四则运算题目 Java实现 by 黄国航 黄宇航
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
,将所有数值用分数来表示,这个分数类只有两个属性molecule
和denominator
,即分子和分母,运算的时候通过分数的分子分母进行运算,所以我们需要一个方法将自然数转化为分数,这个方法是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 黄国航 黄宇航相关推荐
- java实现加减乘除运算符随机生成十道题并判断对错_简单小程序——产生三十道小学四则运算题目...
题目要求程序可以生成三十道小学四则运算题目. 因为要随机生成题目,则需要产生随机数,因此我上网搜索了生成随机数的方法,选择了使用Random类得到规定范围内的随机数.因为一个运算需要三个元素,两个参与 ...
- 【2017下集美大学软工1412班_助教博客】个人作业1——四则运算题目生成程序 成绩公示...
作业要求 个人作业1--四则运算题目生成程序(基于控制台) 使用 -n 参数控制生成题目的个数 使用 -r 参数控制题目中数值 生成的题目中如果存在形如e1 ÷ e2的子表达式,那么其结果应是真分数 ...
- java生成四则运算表达式_生成四则运算(java实现)
|博客班级 | https://edu.cnblogs.com/campus/ahgc/AHPU-SE-19/ | |作业要求 | https://edu.cnblogs.com/campus/ahg ...
- 高级软件工程2017第2次作业—— 个人项目:四则运算题目生成程序(基于控制台)...
Deadline:2017-09-27(周三) 21:00pm (注:以下内容参考 福大软工作业 和集大个人作业 ) 0.前言 很多童鞋在本课程的目标和规划中,都表示希望能提高自己的实践能力. Pra ...
- 结对编程—四则运算(JAVA)(卢泰佑、李密)
Github项目链接:https://github.com/lutys/arithmetic 一.项目简介 项目要求实现一个自动生成小学四则运算题目的命令行程序. 自然数:0, 1, 2, -. 真分 ...
- postfixcalc函数 java_结对编程--四则运算(Java)萧英杰 夏浚杰
结对编程--四则运算(Java)萧英杰 夏浚杰 功能要求 题目:实现一个自动生成小学四则运算题目的命令行程序 使用 -n 参数控制生成题目的个数(实现) 使用 -r 参数控制题目中数值(自然数.真分数 ...
- 四则运算游戏 java代码_四则运算程序(java基于控制台)
一.题目描述: 1. 使用 -n 参数控制生成题目的个数,例如 Myapp.exe -n 10 -o Exercise.txt 将生成10个题目. 2. 使用 -r 参数控制题目中数值(自然数.真分数 ...
- myapp——自动生成小学四则运算题目的命令行程序(侯国鑫 谢嘉帆)
1.Github项目地址 https://github.com/baiyexing/myapp.git 2.功能要求 题目:实现一个自动生成小学四则运算题目的命令行程序 功能(已全部实现) 使用 -n ...
- 个人作业1 四则运算题目生成程序
项目地址:https://gitee.com/wenguixin/javascript_four_algorithms.git 1.题目描述: 生成定量小学四则运算的题目. 2.需求分析: 在现今的时 ...
最新文章
- Key-Value数据库:Redis与Memcached之间如何选择?
- Python爬取B站5000条视频,揭秘为何千万人为它流泪
- linux init进程是所有用户进程的祖先进程,Linux中init进程介绍及常用方法
- Linux系统设置全局的默认网络代理
- 《数据库技术原理与应用教程(第2版)》——习 题 1
- TensorFlow 教程——电影评论文本分类
- radio 取值赋值 亲测有用实效
- javascript经典实例_一道前端经常忽视的JavaScript面试题
- python requests cookie_python requests 带cookie访问页面
- Git 操作总结整合篇
- Spring Boot学习总结(4)——使用Springloaded进行热部署
- javaJavaScript DOM
- POJ- 1751 Highways
- c语言记账系统源程序,C语言会计记账管理系统.doc
- c语言把数字转换为字母,C语言将字符串转数字
- 计算机管理五大功能,操作系统五大管理功能包括哪些介绍大全
- power apps canvas团队协作开发总结的几种方式
- wireshark:包重组
- 两种重要的数据【逻辑数据模型,概念数据模型】
- 线程池为啥要用阻塞队列