结对项目:自动生成四则运算
项目成员:
迪力亚尔·热夏提 3121005122
秋奇昊 3121004321
github地址:作业仓库
一、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 30 |
Estimate | 估计这个任务需要多少时间 | 30 | 10 |
Development | 开发 | 300 | 300 |
Analysis | 需求分析 (包括学习新技术) | 60 | 30 |
Design Spec | 生成设计文档 | 60 | 60 |
Design Review | 设计复审 | 15 | 10 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 15 | 10 |
Design | 具体设计 | 30 | 60 |
Coding | 具体编码 | 30 | 60 |
Code Review | 代码复审 | 15 | 10 |
Test | 测试(自我测试,修改代码,提交修改) | 15 | 60 |
Reporting | 报告 | 60 | 120 |
Test Report | 测试报告 | 30 | 60 |
Size Measurement | 计算工作量 | 10 | 10 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 10 | 20 |
合计 | 740 | 850 |
二、项目结构
- Main类:主程序入口
- CheckUtils类:检查答题对错情况和操作符顺序
- FileUtils类:用于读取文件
- GenerateUtils类:最主要的工具类,用于生成题目等等
- SaveUtils类:保存答案到文件等等操作
三、设计思路和实现过程
- 命令行参数解析部分:根据命令行参数的不同,程序执行相应的功能。通过判断参数个数和参数值来确定执行生成题目、检查答案还是输出帮助信息。
- 题目生成部分:通过随机数生成器Random,循环生成指定数量的四则运算式子。每个式子由随机生成的操作数和操作符构成,操作符从预定义的OPERATORS数组中随机选择。
- 数字生成部分:利用随机数生成器Random生成随机的分子和分母,并根据范围限制确定整数部分和是否有分数部分。根据不同情况,将数字以特定格式转化为字符串表示。
- 题目保存部分:通过BufferedWriter将生成的题目保存到名为"Exercises.txt"的文本文件中。同时,使用计数器变量保持题目序号,方便阅读。
- 答案计算和保存部分:根据生成的题目,将每个题目表达式按照空格拆分为运算项,通过后缀表达式转换和求值的方法计算每个题目的结果。使用BufferedWriter将答案保存到名为"Answers.txt"的文本文件中。
- 用户回答问题并写入自己答案的部分:根据生成的题目,把自己对于题目的回答用BufferedWriter写入”AnswersOfMine.txt”文件中。
- 后缀表达式转换部分:通过两个栈,一个保存运算项的后缀表达式(postfix),另一个保存运算符的栈(operatorStack),遍历每个运算项,根据运算项是运算符还是操作数进行相应的处理和判断。最后,将运算符栈中剩余的运算符按照顺序弹出,放入后缀表达式栈中。
- 答案检查部分:通过读取题目文件和答案文件的内容,并逐行进行比较,对每个题目求解并与答案比对,统计正确和错误数量。通过两个StringBuilder分别保存正确和错误题目的序号,方便最后输出成绩单。
- 文件读写部分:使用BufferedReader和BufferedWriter实现文件的读取和写入操作,其中使用try-with-resources语句,自动关闭资源,提高代码的可读性和简洁性。
- 成绩单保存部分:通过BufferedWriter将正确题目数量、正确题目的序号列表、错误题目数量和错误题目的序号列表保存到名为"Grade.txt"的文本文件中。
后缀表达式的转换:
通过使用两个栈来实现后缀表达式的转换。
- 首先,定义一个栈 operatorStack ,用于保存运算符。
遍历输入的中缀表达式,对于每个运算项(数字或运算符)执行以下操作:
-
如果当前运算项是数字,则直接将其输出到后缀表达式栈 postfix 中。
-
如果当前运算项是左括号"(",则将其入栈。
-
如果当前运算项是右括号")",则将 operatorStack 栈顶的运算符弹出并输出到后缀表达式栈 postfix 中,直到遇到左括号"("为止,然后将左括号从 operatorStack 中弹出,但不输出到 postfix 中。
-
如果当前运算项是运算符,比较其与 operatorStack 栈顶运算符的优先级:如果 operatorStack 栈顶的运算符优先级大于等于当前运算符,将 operatorStack 栈顶的运算符弹出并输出到 postfix 中,重复此步骤,直到栈顶运算符优先级小于当前运算符或栈为空。
-
将当前运算符入栈。
-
当所有运算项遍历完毕后,将 operatorStack 中剩余的运算符依次弹出并输出到 postfix 中。
-
转换完成后,栈 operatorStack 中剩余的运算符就是按照优先级逆序排列的后缀表达式。
代码如下:
/**
* 计算表达式的函数
*/
public static double calculateExpression(String[] terms) {
List<String> postfix = infixToPostfix(terms); // 将中缀表达式转换为后缀表达式
return evaluatePostfixExpression(postfix); // 计算后缀表达式的值
}
/**
* 将表达式转换为后缀表达式
*/
private static List<String> infixToPostfix(String[] terms) {
List<String> postfix = new ArrayList<>();
List<String> operatorStack = new ArrayList<>();
for (String term : terms) {
if (isOperator(term)) {
//用栈来存储原表达式中操作符的顺序
while (!operatorStack.isEmpty() && isOperator(operatorStack.get(operatorStack.size() - 1))) {
String topOperator = operatorStack.get(operatorStack.size() - 1);
if (CheckUtils.compareOperatorPrecedence(topOperator, term) >= 0) {
postfix.add(operatorStack.remove(operatorStack.size() - 1));
} else {
break;
}
}
operatorStack.add(term);
} else {
postfix.add(term);
}
}
while (!operatorStack.isEmpty()) {
postfix.add(operatorStack.remove(operatorStack.size() - 1));
}
return postfix;
}
/**
* 计算后缀表达式的值
* @return double 返回值
*/
private static double evaluatePostfixExpression(List<String> postfix) {
List<Double> operandStack = new ArrayList<>();
for (String term : postfix) {
if (isOperator(term)) {
double operand2 = operandStack.remove(operandStack.size() - 1);
double operand1 = operandStack.remove(operandStack.size() - 1);
// 根据运算符进行计算
switch (term) {
case "+":
operandStack.add(operand1 + operand2);
break;
case "-":
operandStack.add(operand1 - operand2);
break;
case "*":
operandStack.add(operand1 * operand2);
break;
case "÷":
operandStack.add(operand1 / operand2);
break;
}
} else {
operandStack.add(parseNumber(term));
}
}
return operandStack.get(0);
}
四、效能分析
Override如下:
内存分布如下:
由于操作时要将数字大量的转为字符串,同时在判断算数运算符的时候需要使用到char,因此char类型和string类型占用的空间内存最大
五、测试与运行
测试代码如下:
public class MainTest {
@Test
public void mainTest(){
// 生成10个题目以及范围为6
System.out.println(">>>生成问题");
System.out.println();
String[] args = {"-n","10","-r","6"};
Main.main(args);
System.out.println(">>>回答问题");
System.out.println();
// 第二个分支
String[] args2 = {"-e","src/main/java/com/jiedui/test/Exercises.txt","-a","src/main/java/com/jiedui/test/AnswersOfMine.txt"};
Main.main(args2);
// 异常分支
System.out.println(">>>故意进入异常分支");
Main.main(new String[0]);
}
}
测试结果如下:
覆盖率如下:
生成的文件如下:
第一次:
(利用命令行参数输入题目个数n=5,数值范围r=4,自动生成题目和答案)
(生成的5个题目)
(题目对应的答案)
(输入自己的回答)
(利用命令行参数检查自己的答题情况)
(答题情况)
第二次:
(利用命令行参数输入题目个数n=10,数值范围r=6,自动生成题目和答案)
(生成的10个题目)
(题目对应的答案)
(输入自己的回答)
(利用命令行参数检查自己的答题情况)
(答题情况)
六、项目小结
秋奇昊:负责了大部分的代码设计以及编码规范等等。同时在写代码的时候由于有同伴的提醒和纠错,帮助提高了自身素质和代码质量。同时,在设计项目初始的时候,合作思考可以考虑的更加全面。
迪力亚尔·热夏提:主要负责文档编写和代码测试,以及代码注释的编写等等。在代码测试的时候,可以帮助找到代码中的一些bug,也为使用idea的调试功能更加熟练。