一 、Github项目地址:https://github.com/734635746/MyApp
二、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | ||
· Analysis | · 需求分析 | 120 | 120 |
· Design Spec | · 生成设计文档 | 60 | 70 |
· Design Review | · 设计复审 | 40 | 60 |
· Coding Standard | · 代码规范 | 30 | 40 |
· Design | · 具体设计 | 100 | 90 |
· Coding | · 具体编码 | 800 | 800 |
· Code Review | · 代码复审 | 60 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | ||
Reporting | 报告 | ||
· Test Report | · 测试报告 | ||
· Size Measurement | · 计算工作量 | ||
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | ||
合计 |
三、效能分析
@谢飞机
四、设计实现过程
在阅读完题目后,我们两个人一开始是想通过设计一个运算式的类来处理。将随机生成的运算符和操作数作为类的属性,然后通过类的方法来进行相应的处理计算。但最后还是选择了直接生成运算式的字符串来处理。这样处理起来会更加直接方便。
具体的思路是接收-n和-r参数来控制运算式的数量和数值的范围。通过随机生成的运算符和操作数来生成运算式字符串。在生成运算式字符串的过程中可以通过随机数来随机生成带括号的运算式。接下来就是这个项目的难点所在如何计算随机生成运算式的结果数值。
因为运算式的运算符个数和类型是随机的(1~3个),运算符的优先级也有高低之分,以及括号带来的运算先后顺序也不一样。这就使得结果的计算变得复杂起来,最终通过查阅资料发现这个问题可以通过调度场算法来解决。调度场算法可以是一个用于将中缀表达式转换为后缀表达式的经典算法,通过这个算法可以解决运算式的结果计算难题。
在这个项目中,所有的操作数包括整数在计算的时候都作为分数进行运算(整数的分母为1)。为此设计了一个分数类(Fraction)来进行处理,包括分数的整数部分、分子部分、分母部分。这样做的好处是在两个操作数进行运算的时候可以通过分数运算规则来进行。
项目结构如下图:
代码结构功能说明:
Fraction : 分数类,项目中所有的操作数在计算的时候都转化成Fraction对象进行计算
SymbolConstant :常量类,定义了项目中用到的常量
CalculateUtil : 运算工具类,封装了运算表答式计算所需要的方法
ExpressionUtil :运算表达式工具类,封装了生成运算式的所需方法
NumberUtil : 操作数工具类,封装了用于操作数生成的方法
OperatorUtil : 封装了生成运算符所需要的方法
PrintFileUtil :封装了用于生成题目文件以及答案验证的方法
Main :主类,用于接收参数调用具体功能
调用关系流程图:
五、 代码说明
分数类
public class Fraction { private int inter;//整数部分 private int numerator;//分子 private int denominator;//分母 //省略构造方法 set、get方法 }
运算式生成的代码
/** * 根据运算符数组和操作数数组生成运算式表达式 * @param curOperators 运算符数组 * @param curNumbers 操作数数组 * @return 运算式字符串 */ private static String getExpressStr(Character[] curOperators, String[] curNumbers){ ---->运算符数组和操作数数组是随机生成的,由于生成的相关代码比较简单这里就不展示了 //操作数的数量 int number = curNumbers.length; //随机判断是否生成带括号的运算式 int isAddBracket = (int)(Math.random()*10) % 2; -----> 该运算式是否带括号也是通过随机数来判断的 //随机生成器 Random random = new Random(); if(isAddBracket==1){//生成带括号的表答式 //当标记为1时代表该操作数已经添加了左括号 int[] lStamp = new int[number]; --------->这两个数组是用来标记当前操作数是否添加了左、右括号 //当标记为1时代表该操作数已经添加了右括号 int[] rStamp = new int[number]; //遍历操作数数组,随机添加括号 for (int index=0;index<number-1;index++) { -------------->遍历操作数来随机添加左括号,这里没有遍历到最后一个操作数是由于最后一个操作数不可能添加左括号 int n = (int)(Math.random()*10) % 2; if(n == 0 && rStamp[index] != 1) {//判断当前操作数是否标记了左括号 lStamp[index] = 1;//标记左括号 curNumbers[index] = "(" + curNumbers[index]; //操作数之前加上左括号 int k = number - 1; //生成右括号的位置 int rbracketIndex = random.nextInt(k)%(k-index) + (index+1); //如果当前操作数有左括号,则重新生成右括号位置 while (lStamp[rbracketIndex] == 1){ rbracketIndex = random.nextInt(k)%(k-index) + (index+1); }
rStamp[rbracketIndex] = 1; curNumbers[rbracketIndex] = curNumbers[rbracketIndex] +")"; } } } //将运算符数组和操作数数组拼成一个运算式字符串 StringBuilder str = new StringBuilder(curNumbers[0]); for (int k = 0; k < curOperators.length; k++) { str.append(curOperators[k]).append(curNumbers[k + 1]); } return str.toString(); }
运算式结果计算相关代码
/** * @author liuyoubin * @date 2019/9/28 - 15:06 * 运算工具类 */ public class CalculateUtil {
运算式的结果计算采用了调度场算法吗,该算法的思想是将我们常见的中缀表达式转成后缀表达式。算法如下:
-
当还有记号可以读取时:
-
读取一个记号。
-
如果这个记号表示一个数字,那么将其添加到输出队列中。
-
如果这个记号表示一个函数,那么将其压入栈当中。
-
如果这个记号表示一个函数参数的分隔符(例如,一个半角逗号,):
-
从栈当中不断地弹出操作符并且放入输出队列中去,直到栈顶部的元素为一个左括号为止。如果一直没有遇到左括号,那么要么是分隔符放错了位置,要么是括号不匹配。
-
如果这个记号表示一个操作符,记做o1,那么:
-
只要存在另一个记为o2的操作符位于栈的顶端,并且
-
如果o1是左结合性的并且它的运算符优先级要小于或者等于o2的优先级,或者
-
如果o1是右结合性的并且它的运算符优先级比o2的要低,那么
-
将o2从栈的顶端弹出并且放入输出队列中(循环直至以上条件不满足为止);
-
然后,将o1压入栈的顶端。
-
如果这个记号是一个左括号,那么就将其压入栈当中。
-
如果这个记号是一个右括号,那么:
-
从栈当中不断地弹出操作符并且放入输出队列中,直到栈顶部的元素为左括号为止。
-
将左括号从栈的顶端弹出,但并不放入输出队列中去。
-
如果此时位于栈顶端的记号表示一个函数,那么将其弹出并放入输出队列中去。
-
如果在找到一个左括号之前栈就已经弹出了所有元素,那么就表示在表达式中存在不匹配的括号。
-
当再没有记号可以读取时:
-
如果此时在栈当中还有操作符:
-
如果此时位于栈顶端的操作符是一个括号,那么就表示在表达式中存在不匹配的括号。
-
将操作符逐个弹出并放入输出队列中。
-
退出算法
-
/** * 采用调度场算法,获取指定运算式的结果值 * * @param express 运算式 * @return */ public static String getExpressValue(String express){ //运算符栈,用于存放运算符包括 +、-、*、÷、(、) Stack<Character> operators = new Stack<Character>(); //操作数栈,用于存放操作数 Stack<Fraction> fractions = new Stack<Fraction>(); //将表达式字符串转成字符数组 char[] chars = express.toCharArray(); //遍历获取处理 for (int i=0;i<chars.length;i++) { //获取当前的字符 char c = chars[i]; if(c=='('){//如果是左括号,入栈 operators.push(c); }else if(c==')'){//当前字符为右括号 //当运算符栈顶的元素不为‘(’,则继续 while(operators.peek()!='('){ //拿取操作栈中的两个分数 Fraction fraction1 = fractions.pop(); Fraction fraction2 = fractions.pop(); //获取计算后的值 Fraction result = calculate(operators.pop(), fraction1.getNumerator(), fraction1.getDenominator(), fraction2.getNumerator(), fraction2.getDenominator()); //将结果压入栈中 fractions.push(result); } //将左括号出栈 operators.pop(); }else if(c=='+'||c=='-'||c=='*'||c=='÷'){//是运算符 //当运算符栈不为空,且当前运算符优先级小于栈顶运算符优先级 while(!operators.empty()&&!priority(c, operators.peek())){ //拿取操作栈中的两个分数 Fraction fraction1 = fractions.pop(); Fraction fraction2 = fractions.pop(); //获取计算后的值 Fraction result = calculate(operators.pop(), fraction1.getNumerator(), fraction1.getDenominator(), fraction2.getNumerator(), fraction2.getDenominator()); //将结果压入栈中 fractions.push(result); } //将运算符入栈 operators.push(c); }else{//是操作数 if(c>='0'&&c<='9'){ StringBuilder buf = new StringBuilder(); //这一步主要是取出一个完整的数值 比如 2/5、9、9/12 while(i<chars.length&&(chars[i]=='/'||((chars[i]>='0')&&chars[i]<='9'))){ buf.append(chars[i]); i++; } i--; //到此 buf里面是一个操作数 String val = buf.toString(); //标记‘/’的位置 int flag = val.length(); for(int k=0;k<val.length();k++){ if(val.charAt(k)=='/'){//当获取的数值存在/则标记/的位置,便于接下来划分分子和分母生成分数对象 flag = k; } } //分子 StringBuilder numeratorBuf = new StringBuilder(); //分母 StringBuilder denominatorBuf = new StringBuilder(); for(int j=0;j<flag;j++){ numeratorBuf.append(val.charAt(j)); } //判断是否为分数 if(flag!=val.length()){ for(int q=flag+1;q<val.length();q++){ denominatorBuf.append(val.charAt(q)); } }else{//如果不是分数则分母计为1 denominatorBuf.append('1'); } //入栈 fractions.push(new Fraction(Integer.parseInt(numeratorBuf.toString()), Integer.parseInt(denominatorBuf.toString()))); } } } while(!operators.empty()){ Fraction fraction1 = fractions.pop(); Fraction fraction2 = fractions.pop(); //获取计算后的值 Fraction result = calculate(operators.pop(), fraction1.getNumerator(), fraction1.getDenominator(), fraction2.getNumerator(), fraction2.getDenominator()); //将结果压入栈中 fractions.push(result); } //计算结果 Fraction result = fractions.pop(); //获取最终的结果(将分数进行约分) return getFinalResult(result); } --------------------------------------------------------------------------------------------------------------------------------------------- private static String getFinalResult(Fraction result) { if(result.getDenominator()==0){ return "0"; } //获取最大公约数 int gcd = gcd(result.getNumerator(),result.getDenominator()); if(result.getDenominator()/gcd==1){//分母为1 return String.valueOf(result.getNumerator()/gcd); }else{ //如果分子大于分母则化成真分数的形式 if(result.getNumerator()>result.getDenominator()){ result = getRealFraction(result); return result.getInter()+"'"+result.getNumerator()/gcd+"/"+result.getDenominator()/gcd; }else{ return result.getNumerator()/gcd+"/"+result.getDenominator()/gcd; } } } ----------------------------------------------------------------------------------------------------------------------------------------------- /** * 化成真分数 * @param result * @return */ private static Fraction getRealFraction(Fraction result){ int numerator = result.getNumerator(); int denominator = result.getDenominator(); //计算分子部分 int newNumerator = numerator % denominator; //计算整数部分 int inter = numerator/denominator; Fraction fraction = new Fraction(newNumerator, denominator); fraction.setInter(inter); ------------------------------->整数部分 return fraction; } ------------------------------------------------------------------------------------------------------------------------------------------------- /** * 判断两个运算符的优先级 * 当opt1的优先级大于opt2时返回true * 这是根据调度场算法给出的实现 * @return */ private static boolean priority(char opt1,char opt2){ --------------------------->只有当opt1的优先级小于或等于opt2的优先级时才放回true。这是根据调度场算法给出的实现 if((opt1=='+'||opt1=='-')&&(opt2=='*'||opt2=='÷')){ return false; }else if((opt1=='+'||opt1=='-')&&(opt2=='+'||opt2=='-')){ return false; }else if((opt1=='*'||opt1=='÷')&&(opt2=='*'||opt2=='÷')){ return false; }else{ return true; } } ---------------------------------------------------------------------------------------------------------------------------------------------------- /** * 对两个分数进行相应的运算,获取结果 * @param opt 运算符 * @param num1 分子1 * @param den1 分母1 * @param num2 分子2 * @param den2 分母2 * @return 结果 */ private static Fraction calculate(Character opt,int num1,int den1,int num2,int den2){ //结果数组,存放结果的分子与分母 int[] result = new int[2]; /** * 这里有一个陷阱,因为用于计算的两个数是通过栈来存储,所以取出计算结果的时候两个数的顺序会颠倒 * 比如式子 1/2*9/12 我取出来的时候其实是 9/12 和 1/2 所以调用此函数的时候是calculate('*',9,12,1,2),所以下面的实现要注意不要踩坑 */ switch (opt){ case '+': result[0] = num1*den2 + num2*den1; result[1]= den1*den2; return new Fraction(result[0],result[1]); case '-': result[0] = num2*den1 - num1*den2; result[1]= den1*den2; return new Fraction(result[0],result[1]); case '*': result[0] = num1*num2; result[1] = den1*den2; return new Fraction(result[0],result[1]); case '÷': result[0] = num2*den1; result[1] = num1*den2; return new Fraction(result[0],result[1]); } return new Fraction(result[0],result[1]); } --------------------------------------------------------------------------------------------------------------------------------------------- /** * 获取分子分母的最大公约数,辗转相除法 * @param numerator 分子 * @param denominator 分母 * @return 最大公约数 */ private static int gcd(int numerator,int denominator){ numerator = Math.abs(numerator); denominator = Math.abs(denominator); while (denominator != 0) { // 求余 int remainder = numerator % denominator; // 交换数,等同递归调用 numerator = denominator; denominator = remainder; } return numerator; } }
六、测试运行
@谢飞机