结对项目(java实现)
Posted 何处似樽前
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了结对项目(java实现)相关的知识,希望对你有一定的参考价值。
一 、Github项目地址:https://github.com/734635746/MyApp
二、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 1310 | 1460 |
· Analysis | · 需求分析 | 120 | 120 |
· Design Spec | · 生成设计文档 | 60 | 70 |
· Design Review | · 设计复审 | 40 | 60 |
· Coding Standard | · 代码规范 | 30 | 40 |
· Design | · 具体设计 | 100 | 90 |
· Coding | · 具体编码 | 800 | 900 |
· Code Review | · 代码复审 | 60 | 80 |
· Test | · 测试(自我测试,修改代码,提交修改) | 100 | 100 |
Reporting | 报告 | 130 | 150 |
· Test Report | · 测试报告 | 60 | 80 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 40 | 40 |
合计 | 1470 | 1640 |
三、效能分析
1. 使用JProfiler进行效能分析,查看cpu时间,进行针对性的优化。
2. 优化过程:
a. 在获取结果的getFinalResult中,取消重复调用Fraction类的get方法。(1.0%->0.4% 约快10s/1million条)
b. 在对结果的化简时,使用非递归获取最大公约数。(5.4%->1.2% 约快400ms/10万条)
c. 计算过程中对一次计算(两个操作数一个运算符)中,取消重复调用Fraction类的构造方法生成结果。(2.6%->2.1% 约快40ms/10万条)
3. 优化前后:生成一百万条十以内的题目(-n 1000000 -r 10)
四、设计实现过程
在阅读完题目后,我们两个人一开始是想通过设计一个运算式的类来处理。将随机生成的运算符和操作数作为类的属性,然后通过类的方法来进行相应的处理计算。但最后还是选择了直接生成运算式的字符串来处理。这样处理起来会更加直接方便。
具体的思路是接收-n和-r参数来控制运算式的数量和数值的范围。通过随机生成的运算符和操作数来生成运算式字符串。在生成运算式字符串的过程中可以通过随机数来随机生成带括号的运算式。接下来就是这个项目的难点所在如何计算随机生成运算式的结果数值。
因为运算式的运算符个数和类型是随机的(1~3个),运算符的优先级也有高低之分,以及括号带来的运算先后顺序也不一样。这就使得结果的计算变得复杂起来,最终通过查阅资料发现这个问题可以通过调度场算法来解决。调度场算法可以是一个用于将中缀表达式转换为后缀表达式的经典算法,通过这个算法可以解决运算式的结果计算难题。
在这个项目中,所有的操作数包括整数在计算的时候都作为分数进行运算(整数的分母为1)。为此设计了一个分数类(Fraction)来进行处理,包括分数的整数部分、分子部分、分母部分。这样做的好处是在两个操作数进行运算的时候可以通过分数运算规则来进行。
项目支持通过-r -n 参数命令随机(运算符1~3个,操作数在-n所指定的范围内,括号随机)生成指定数量和数值范围的题目文件(Exercises.txt)和答案文件(Answers.txt),支持通过-e -a 参数命令验证用户所提交答案文件的正确程度,并生成结果文件(Grade.txt)。
项目结构如下图:
代码结构功能说明:
Fraction : 分数类,项目中所有的操作数在计算的时候都转化成Fraction对象进行计算
SymbolConstant :常量类,定义了项目中用到的常量
CalculateUtil : 运算工具类,封装了运算表答式计算所需要的方法
ExpressionUtil :运算表达式工具类,封装了生成运算式的所需方法
NumberUtil : 操作数工具类,封装了用于操作数生成的方法
OperatorUtil : 封装了生成运算符所需要的方法
PrintFileUtil :封装了用于生成题目文件以及答案验证的方法
ValidateUtil : 封装了重要数据的检查方法
Main :主类,用于接收参数调用具体功能
调用关系流程图:
说明:1. 主类在接受用户的-r -n/-e -a参数后首先会调用ValidateUtil工具类的checkParams(String command)方法进行参数合法性校验(包括对参数顺序的支持)。
2. 如果是-r -n参数命令
2.1 命令程序会调用ExpressionUtil工具类的generate(int n,int round)方法获取指定数量和参数范围的运算式以及答案的Map集合。
2.2 ExpressionUtil工具类的generate(int n,int round)方法依赖OperatorUtil工具类的getOperators(int num)方法和NumberUtil工具类的getNumbers(int num, int round)获取随机的运算符和操作数,同时还在generate方法里面进行负数以及查重的解决。
2.3 generate(int n,int round)方法依赖同时还需要getExpressValue(String express)获取题目的答案
2.4 在获取到运算式以及答案的集合后程序会调用PrintFileUtil工具类的printExerciseFileAndAnswerFile(Map<String, String> questionAndResultMap) 进行题目文件和答案文件的生成
3. 如果是-e -a参数命令
3.1 程序会调用PrintFileUtil工具类的validateAnswerFile(String exerciseFileUrl, String answerFileUrl)方法进行题目的验证。
3.2 在这个过程中需要通过调CalculateUtil工具类的getExpressValue(String express)获取运算式的答案与用户提交的答案进行比较。
五、 代码说明
分数类
项目的所有数值运算都会化成分数形式进行运算,整数则分母部分为1,分数类的整数部分是用于真分数的生成
1 public class Fraction { 2 private int inter;//整数部分 3 private int numerator;//分子 4 private int denominator;//分母 5 6 //省略构造方法 set、get方法 7 }
项目所用常量
1 public class SymbolConstant { 2 public static final Character PLUS = \'+\'; 3 public static final Character MINUS = \'-\'; 4 public static final Character MULTIPLY = \'*\'; 5 public static final Character DIVIDE = \'÷\'; 6 public static final Character EQUALS = \'=\'; 7 public static final String PRINT_FILE_URL = System.getProperty("user.dir")+ File.separator+"question_bank";//"F:\\\\file";10.3修改生成文件地址 8 9 }
随机操作数和运算符的生成代码
运算符的类型和数量(num)是随机且符合要求的,操作数的数量(num+1)、数值类型以及数值范围也是随机且合法的
1 class NumberUtil { 2 3 /** 4 * 随机获取num个操作数的数组 5 */ 6 static String[] getNumbers(int num, int round) { 7 8 Random random = new Random(); 9 String[] numbers = new String[num]; 10 11 for (int i = 0; i < num; i++) { 12 //用于判断生成整数还是分数 13 int flag = (int)(Math.random()*10) % 2; 14 15 if(flag==0){//生成整数 16 int n = random.nextInt(round); 17 numbers[i] = (n==0?1:n)+""; 18 }else{//生成分数 19 //随机生成分子和分母,为了避免分子分母生成0进行了+1的改进 20 int numerator = (random.nextInt(round))+1; 21 int denominator = (random.nextInt(round))+1;; 22 23 while(numerator>=denominator||numerator==0||denominator==0){//判断是否为真分数,且不能生成带0的分数 24 numerator = (random.nextInt(round))+1; 25 denominator = (random.nextInt(round))+1; 26 } 27 //拼装成分数形式 28 numbers[i] = numerator+"/"+denominator; 29 } 30 } 31 return numbers; 32 } 33 34 } 35 36 public class OperatorUtil { 37 38 private final static Character[] operatorTypes = new Character[]{SymbolConstant.PLUS,SymbolConstant.MINUS,SymbolConstant.MULTIPLY,SymbolConstant.DIVIDE}; 39 40 /** 41 * 随机获取num个运算符的数组 42 */ 43 static Character[] getOperators(int num) { 44 45 Character[] operators = new Character[num]; 46 47 for (int i = 0; i < num; i++) { 48 //随机获取运算符的类型(0~3 代表4个运算符的类型) 49 int operatorTypeIndex = (int)(Math.random()*4); 50 Character operatorType = operatorTypes[operatorTypeIndex]; 51 operators[i] = operatorType; 52 } 53 54 return operators; 55 } 56 57 58 }
运算式生成的代码
1 运算式的生成是根据上述的随机运算符和操作数来确定的,同时在生成运算式的时候也随机生成括号
2 在生成运算式后会调用结果生成的方法进行结果的获取同时验证运算式及其结果的正确性
1 /** 2 * @author liuyoubin 3 * @date 2019/9/27 - 22:09 4 */ 5 public class ExpressionUtil { 6 7 /** 8 * 获取指定个数和数值范围的运算式字符串和结果 9 */ 10 public static Map<String,String> generate(int n,int round){ 11 12 //运算式和结果的集合 13 Map<String,String> questionAndResultMap = new HashMap<String,String>(); 14 //结果集合,用于判断是否重复 15 Set<String> result = new HashSet<String>(); 16 for (int i = 0; i < n; i++) { 17 //随机获取运算符的个数(1~3个) 18 int num = (int)(Math.random()*3)+1; 19 //随机获取num个运算符 20 Character[] curOperators = OperatorUtil.getOperators(num); 21 //随机获取num+1个操作数 22 String[] curNumbers = NumberUtil.getNumbers(num+1,round); 23 //获取运算式表达式 24 String[] questionAndResult = getExpressStr(curOperators, curNumbers); 25 26 if(questionAndResult==null){//判断运算过程是否出现负数 27 i--; 28 }else if (result.contains(questionAndResult[1])){//判断是否重复 29 i--; 30 }else { 31 result.add(questionAndResult[1]); 32 questionAndResultMap.put(questionAndResult[0],questionAndResult[1]); 33 } 34 } 35 return questionAndResultMap; 36 } 37 38 /** 39 * 根据运算符数组和操作数数组生成运算式表达式 40 * @param curOperators 运算符数组 41 * @param curNumbers 操作数数组 42 * @return 运算式字符串以及其结果 43 */ 44 private static String[] getExpressStr(Character[] curOperators, String[] curNumbers){ ---->运算符数组和操作数数组是随机生成的 45 //操作数的数量 46 int number = curNumbers.length; 47 //随机判断是否生成带括号的运算式 48 int isAddBracket = (int)(Math.random()*10) % 2; -----> 该运算式是否带括号也是通过随机数来判断的 49 //随机生成器 50 Random random = new Random(); 51 52 if(isAddBracket==1){//生成带括号的表达式 53 //当标记为1时代表该操作数已经添加了左括号 --------->这两个数组是用来标记当前操作数是否添加了左、右括号 54 int[] lStamp = new int[number]; 55 //当标记为1时代表该操作数已经添加了右括号 56 int[] rStamp = new int[number]; 57 //遍历操作数数组,随机添加括号 58 for (int index=0;index<number-1;index++) { -------------->遍历操作数来随机添加左括号,这里没有遍历到最后一个操作数是由于最后一个操作数不可能添加左括号 59 int n = (int)(Math.random()*10) % 2; 60 if(n == 0 && rStamp[index] != 1) {//判断当前操作数是否标记了左括号 61 lStamp[index] = 1;//标记左括号 62 curNumbers[index] = "(" + curNumbers[index]; //操作数之前加上左括号 63 int k = number - 1; 64 //生成右括号的位置 65 int rbracketIndex = random.nextInt(k)%(k-index) + (index+1); 66 //如果当前操作数有左括号,则重新生成优括号位置 67 while (lStamp[rbracketIndex] == 1){ 68 rbracketIndex = random.nextInt(k)%(k-index) + (index+1); 69 } 70 rStamp[rbracketIndex] = 1; 71 curNumbers[rbracketIndex] = curNumbers[rbracketIndex] +")"; 72 73 } 74 } 75 } 76 77 //将运算符数组和操作数数组拼成一个运算式字符串 78 StringBuilder str = new StringBuilder(curNumbers[0]); 79 for (int k = 0; k < curOperators.length; k++) { 80 str.append(curOperators[k]).append(curNumbers[k + 1]); 81 } 82 //生成的运算式 83 String express = str.toString(); 84 //获取运算式结果 85 String value = CalculateUtil.getExpressValue(express); 86
87 if(value.equals("#")){//运算过程出现负数
88 return null;
89 } 90 return new String[]{express,value}; 91 92 } 93 }
运算式结果计算相关代码
1 运算式的结果生成是根据调度场算法给出的实现
2 在进行数值运算的时候会同一化成分数的形式,根据分数运算式规则进行运算
3 同时还进行了结果的处理,通过辗转相除法生成的最大公约数来计算真分数,同时化成符合规范表达结果字 1 /**
2 * @author liuyoubin 3 * @date 2019/9/28 - 15:06 4 * 运算工具类 5 */ 6 public class CalculateUtil { 7 8 9 运算式的结果计算采用了调度场算法吗,该算法的思想是将我们常见的中缀表达式 转成后缀表达式。算法如下: 10 + 当还有记号可以读取时: 11 -读取一个记号。 12 - 如果这个记号表示一个数字,那么将其添加到输出队列中。 13 - 如果这个记号表示一个函数,那么将其压入栈当中。 14 -如果这个记号表示一个函数参数的分隔符(例如,一个半角逗号,): 15 -从栈当中不断地弹出操作符并且放入输出队列中去,直到栈顶部的元素为一个左括号为止。如果一直没有遇到左括号,那么要么是分隔符放错了位置,要么是括号不匹配。 16 + 如果这个记号表示一个操作符,记做o1,那么: 17 -只要存在另一个记为o2的操作符位于栈的顶端,并且 18 -如果o1是左结合性的并且它的运算符优先级要小于或者等于o2的优先级,或者 19 -如果o1是右结合性的并且它的运算符优先级比o2的要低,那么 20 -将o2从栈的顶端弹出并且放入输出队列中(循环直至以上条件不满足为止); 21 + 然后,将o1压入栈的顶端。 22 + 如果这个记号是一个左括号,那么就将其压入栈当中。 23 + 如果这个记号是一个右括号,那么: 24 -从栈当中不断地弹出操作符并且放入输出队列中,直到栈顶部的元素为左括号为止。 25 -将左括号从栈的顶端弹出,但并不放入输出队列中去。 26 -如果此时位于栈顶端的记号表示一个函数,那么将其弹出并放入输出队列中去。 27 -如果在找到一个左括号之前栈就已经弹出了所有元素,那么就表示在表达式中存在不匹配的括号。 28 +当再没有记号可以读取时: 29 -如果此时在栈当中还有操作符: 30 -如果此时位于栈顶端的操作符是一个括号,那么就表示在表达式中存在不匹配的括号。 31 -将操作符逐个弹出并放入输出队列中。 32 +退出算法 33 /** 34 * 采用调度场算法,获取指定运算式的结果值 35 * 36 * @param express 运算式 37 * @return 38 */ 39 public static String getExpressValue(String express){ 40 //运算符栈,用于存放运算符包括 +、-、*、÷、(、) 41 Stack<Character> operators = new Stack<Character>(); 42 //操作数栈,用于存放操作数 43 Stack<Fraction> fractions = new Stack<Fraction>(); 44 //将表达式字符串转成字符数组 45 char[] chars = express.toCharArray(); 46 //遍历获取处理 47 for (int i=0;i<chars.length;i++) { 48 //获取当前的字符 49 char c = chars[i]; 50 51 if(c==\'(\'){//如果是左括号,入栈 52 operators.push(c); 53 }else if(c==\')\'){//当前字符为右括号 54 //当运算符栈顶的元素不为‘(’,则继续 55 while(operators.peek()!=\'(\'){ 56 //拿取操作栈中的两个分数 57 Fraction fraction1 = fractions.pop(); 58 Fraction fraction2 = fractions.pop(); 59 //获取计算后的值 60 Fraction result = calculate(operators.pop(), fraction1.getNumerator(), fraction1.getDenominator(), 61 fraction2.getNumerator(), fraction2.getDenominator());
62
63 if(result.getNumberator<0){//运算过程出现负数
64 return "#";
65 }
66 62 //将结果压入栈中 63 fractions.push(result); 64 } 65 //将左括号出栈 66 operators.pop(); 67 }else if(c==\'+\'||c==\'-\'||c==\'*\'||c==\'÷\'){//是运算符 68 //当运算符栈不为空,且当前运算符优先级小于栈顶运算符优先级 69 while(!operators.empty()&&!priority(c, operators.peek())){ 70 //拿取操作栈中的两个分数 71 Fraction fraction1 = fractions.pop(); 72 Fraction fraction2 = fractions.pop(); 73 //获取计算后的值 74 Fraction result = calculate(operators.pop(), fraction1.getNumerator(), fraction1.getDenominator(), 75 fraction2.getNumerator(), fraction2.getDenominator());
76
77 if(result.getNumerator()<0){
78 return "#";
79 }
80 76 //将结果压入栈中 77 fractions.push(result); 78 } 79 //将运算符入栈 80 operators.push(c); 81 }else{//是操作数 82 if(c>=\'0\'&&c<=\'9\'){ 83 StringBuilder buf = new StringBuilder(); 84 //这一步主要是取出一个完整的数值 比如 2/5、9、9/12 85 while(i<chars.length&&(chars[i]==\'/\'||((chars[i]>=\'0\')&&chars[i]<=\'9\'))){ 86 buf.append(chars[i]); 87 i++; 88 } 89 i--; 90 //到此 buf里面是一个操作数 91 String val = buf.toString(); 92 //标记‘/’的位置 93 int flag = val.length(); 94 for(int k=0;k<val.length();k++){ 95 if(val.charAt(k)==\'/\'){//当获取的数值存在/则标记/的位置,便于接下来划分分子和分母生成分数对象 96 flag = k; 97 } 98 } 99 //分子 100 StringBuilder numeratorBuf = new StringBuilder(); 101 //分母 102 StringBuilder denominatorBuf = new StringBuilder(); 103 for(int j=0;j<flag;j++){ 104 numeratorBuf.append(val.charAt(j)); 105 } 106 //判断是否为分数 107 if(flag!=val.length()){ 108 for(int q=flag+1;q<val.length();q++){ 109 denominatorBuf.append(val.charAt(q)); 110 } 111 }else{//如果不是分数则分母计为1 112 denominatorBuf.append(\'1\'); 113 } 114 //入栈 115 fractions.push(new Fraction(Integer.parseInt(numeratorBuf.toString()), Integer.parseInt(denominatorBuf.toString()))); 116 } 117 } 118 } 119 120 while(!operators.empty()){ 121 Fraction fraction1 = fractions.pop(); 122 Fraction fraction2 = fractions.pop(); 123 124 //获取计算后的值 125 Fraction result = calculate(operators.pop(), fraction1.getNumerator(), fraction1.getDenominator(), 126 fraction2.getNumerator(), fraction2.getDenominator());
127
128 if(result.getNumberator()<0){
129 return "#";
130 }
131
132 127 128 //将结果压入栈中 129 fractions.push(result); 130 } 131 132 //计算结果 133 Fraction result = fractions.pop(); 134 //获取最终的结果(将分数进行约分) 135 return getFinalResult(result); 136 137 } 138 ----------------------------------------------------------------------------------- 139 private static String getFinalResult(Fraction result) { 140 int denominator = result.getDenominator(); 141 int numerator = result.getNumerator(); 142 if(denominator==0){ 143 return "0"; 144 } 145 //获取最大公约数 146 int gcd = gcd(numerator,denominator); 147 148 if(denominator/gcd==1){//分母为1 149 return String.valueOf(numerator/gcd); 150 }else{ 151 //如果分子大于分母则化成真分数的形式 152 if(result.getNumerator()>denominator){ 153 result = getRealFraction(result); 154 return result.getInter()+"\'"+result.getNumerator()/gcd+"/"+result.getDenominator()/gcd; 155 }else{ 156 return numerator/gcd+"/"+denominator/gcd; 157 } 158 } 159 } 160 ----------------------------------------------------------------------------------- 161 /** 162 * 化成真分数 163 * @param result 164 * @return 165 */ 166 private static Fraction getRealFraction(Fraction result){ 167 int numerator = result.getNumerator(); 168 int denominator = result.getDenominator(); 169 //计算分子部分 170 int newNumerator = numerator % denominator; 171 //计算整数部分 172 int inter = numerator/denominator; 173 Fraction fraction = new Fraction(newNumerator, denominator); 174 fraction.setInter(inter); ------------------------------->整数部分 175 return fraction; 176 } 177 ----------------------------------------------------------------------------------- 178 /** 179 * 判断两个运算符的优先级 180 * 当opt1的优先级大于opt2时返回true 181 * 这是根据调度场算法给出的实现 182 * @return 183 */ 184 private static boolean priority(char opt1,char opt2){ --------------------------->只有当opt1的优先级小于或等于opt2的优先级时才放回true。这是根据调度场算法给出的实现 185 if((opt1==\'+\'||opt1==\'-\')&&(opt2==\'*\'||opt2==\'÷\')){ 186 return false; 187 }else if((opt1==\'+\'||opt1==\'-\')&&(opt2==\'+\'||opt2==\'-\')){ 188 return false; 189 }else if((opt1==\'*\'||opt1==\'÷\')&&(opt2==\'*\'||opt2==\'÷\')){ 190 return false; 191 }else{ 192 return true; 193 } 194 } 195 ----------------------------------------------------------------------------------- 196 /** 197 * 对两个分数进行相应的运算,获取结果 198 * @param opt 运算符 199 * @param num1 分子1 200 * @param结对项目:四则运算题目生成器(Java)