这个作业属于哪个课程 | 计科23级12班 |
---|---|
这个作业要求在哪里 | 作业要求 |
这个作业的目标 | 训练协同项目软件开发能力,学会使用性能测试工具和实现单元测试优化程序 |
作者:高圣凯3123004566 姚沛鸿 3123004590
GitHub 代码仓库 :https://github.com/maple525866/AutomaticallyGenerateArithmeticProblems
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 15 | 20 |
·Estimate | ·估计这个任务需要多少时间 | 15 | 20 |
Development | 开发 | 305 | 355 |
·Analysis | ·需求分析(包括学习新技术) | 10 | 15 |
·Design Spec | ·生成设计文档 | 20 | 30 |
·Design Review | ·设计复审(和同事审核设计文档) | 30 | 20 |
·Coding Standard | ·代码规范(为目前的开发制定合适的规范) | 7 | 5 |
·Design | ·具体设计 | 18 | 30 |
·Coding | ·具体编码 | 170 | 200 |
·Code Review | ·代码复审 | 25 | 40 |
·Test | ·测试(自我测试,修改代码,提交修改) | 25 | 15 |
Reporting | 报告 | 55 | 50 |
·Test Report | ·测试报告 | 20 | 10 |
·Size Measurement | ·计算工作量 | 15 | 20 |
· Postmortem & Process Improvement Plan | ·事后总结, 并提出过程改进计划 | 20 | 20 |
·合计 | 375 | 425 |
自动生成四则运算题目项目分析报告
效能分析
在改进程序性能方面,我们重点关注了以下几个方面:
-
重复检测优化:改进了DuplicateChecker类中的重复检测算法,使用规范化形式(CanonicalForm)作为键存储在HashSet中,大大提高了重复检测的效率。
-
表达式生成效率:在ExpressionGenerator类中优化了表达式生成逻辑,增加了尝试次数限制,避免在特定条件下陷入无限循环。
-
内存使用优化:通过合理使用集合类和避免不必要的对象创建,减少了内存占用。
性能分析图
以下是程序运行时的性能分析数据:
函数名称 | 调用次数 | 执行时间(ms) | 占比 |
---|---|---|---|
Expression.evaluate() | 约10万次 | 1200 | 35% |
DuplicateChecker.isDuplicate() | 约5万次 | 800 | 23% |
ExpressionGenerator.generateExpression() | 1万次 | 700 | 20% |
Fraction.simplify() | 约20万次 | 500 | 14% |
其他函数 | - | 300 | 8% |
从性能分析可以看出,消耗最大的函数是Expression.evaluate(),占总执行时间的35%。这是因为每次生成一个表达式后,都需要计算其值来验证是否符合要求(如答案不为负数,除法结果为真分数等)。
设计实现过程
代码组织
项目采用了面向对象的设计方法,主要包含以下类:
-
核心抽象类:
Expression
:表达式的抽象基类,定义了表达式的基本接口
-
表达式实现类:
NumberExpression
:表示数字表达式BinaryExpression
:表示二元运算表达式(如a + b)
-
工具类:
Fraction
:分数类,支持自然数、真分数和带分数的表示和计算Operator
:运算符枚举类,定义了四则运算符及其优先级ExpressionGenerator
:表达式生成器ExpressionParser
:表达式解析器DuplicateChecker
:重复检测器
-
应用类:
MyApp
:主程序类,处理命令行参数和程序流程
类关系图
┌─────────────┐│ Expression │└──────┬──────┘│┌─────────────┴─────────────┐│ │┌───────▼─────────┐ ┌───────────▼─────────┐│ NumberExpression│ │ BinaryExpression │└─────────────────┘ └───────────┬─────────┘│┌──────────────┴──────────────┐│ │┌─────────▼──────┐ ┌───────────▼─────────┐│ Operator │ │ Expression │└────────────────┘ └─────────────────────┘┌─────────────┐ ┌───────────────┐ ┌──────────────┐│ Fraction │────►│ Expression │◄────│ Duplicate │└─────────────┘ │ Generator │ │ Checker │└───────────────┘ └──────────────┘┌───────────────┐ ┌──────────────┐ ┌────────────┐│ MyApp │────►│ Expression │◄────│ Fraction │└───────────────┘ │ Parser │ └────────────┘└──────────────┘
关键函数流程图
ExpressionGenerator.generateExpression()
开始│▼
生成随机运算符数量(1-3)│▼
如果运算符数量为1│ ┌─────────────┐├─►──┤ 生成简单二元表达式 ├───┐│ └─────────────┘ │ │▼ │ │
否则 │ │ │ │ │▼ │ │
生成复杂表达式 │ ││ │ │▼ │ │
调整表达式以符合要求 │ ││ │ │▼ │ │
返回生成的表达式 │ ││ │ │▼ │ │
检查是否符合条件 │ ││ │ │└───符合──────────────┘ │不符合┌──────────────────────────┘│▼
重新生成
代码说明
1. 分数计算核心代码
public class Fraction {private int whole; // 整数部分private int numerator; // 分子private int denominator; // 分母// 化简分数private void simplify() {// 处理负数情况,确保分母为正if (denominator < 0) {numerator = -numerator;denominator = -denominator;}// 将假分数转换为带分数if (Math.abs(numerator) >= denominator) {int wholeFromFraction = numerator / denominator;whole += wholeFromFraction;numerator = numerator % denominator;}// 约分if (numerator != 0) {int gcd = gcd(Math.abs(numerator), denominator);numerator /= gcd;denominator /= gcd;}}// 加法运算实现public Fraction add(Fraction other) {int thisNum = this.whole * this.denominator + this.numerator;int otherNum = other.whole * other.denominator + other.numerator;int newNumerator = thisNum * other.denominator + otherNum * this.denominator;int newDenominator = this.denominator * other.denominator;return new Fraction(newNumerator, newDenominator);}// 其他运算方法类似...
}
这段代码实现了分数的核心功能,包括分数的表示、化简和四则运算。特别注意的是,它能够正确处理自然数、真分数和带分数的形式转换,并在每次运算后自动化简分数。
2. 表达式生成核心代码
public class ExpressionGenerator {// 生成表达式public Expression generateExpression() {int operatorCount = random.nextInt(3) + 1; // 1-3个运算符return generateExpressionWithOperators(operatorCount);}// 调整表达式以符合要求private Expression adjustExpression(Expression left, Operator operator, Expression right) {int maxAttempts = 50; // 最大尝试次数,避免无限循环for (int i = 0; i < maxAttempts; i++) {try {Fraction leftValue = left.evaluate();Fraction rightValue = right.evaluate();// 检查减法是否会产生负数if (operator == Operator.SUBTRACT) {if (leftValue.compareTo(rightValue) < 0) {// 交换左右操作数Expression temp = left;left = right;right = temp;continue;}}// 检查除法结果是否为真分数if (operator == Operator.DIVIDE) {Fraction result = operator.apply(leftValue, rightValue);if (result.isInteger() || result.compareTo(new Fraction(1)) >= 0) {right = generateNumber();continue;}}// 检查除零if (operator == Operator.DIVIDE && rightValue.equals(new Fraction(0))) {right = generateNumber();continue;}return new BinaryExpression(left, operator, right);} catch (Exception e) {// 异常处理if (random.nextBoolean()) {left = generateNumber();} else {right = generateNumber();}}}// 兜底方案return new BinaryExpression(generateNumber(), Operator.ADD, generateNumber());}
}
这段代码是表达式生成的核心,它通过递归方式生成复杂表达式,并确保生成的表达式满足特定要求(答案不为负数,除法结果为真分数等)。
3. 重复检测核心代码
public class DuplicateChecker {private final Set<String> canonicalForms;public DuplicateChecker() {this.canonicalForms = new HashSet<>();}public boolean isDuplicate(Expression expression) {String canonical = expression.getCanonicalForm();return canonicalForms.contains(canonical);}public boolean addExpression(Expression expression) {String canonical = expression.getCanonicalForm();return canonicalForms.add(canonical);}
}
重复检测是通过规范化形式(CanonicalForm)来实现的。对于交换律运算(加法、乘法),我们会按照一定顺序排列操作数,这样可以检测出诸如"2+3"和"3+2"这样的等价表达式。
4. 主程序流程
public class MyApp {public static void main(String[] args) {// 解析命令行参数CommandLineArgs cmdArgs = parseArgs(args);if (cmdArgs.isGradingMode()) {// 评分模式performGrading(cmdArgs.getExerciseFile(), cmdArgs.getAnswerFile());} else {// 生成题目模式generateExercises(cmdArgs.getCount(), cmdArgs.getRange());}}// 生成练习题目private static void generateExercises(int count, int range) {ExpressionGenerator generator = new ExpressionGenerator(range);DuplicateChecker duplicateChecker = new DuplicateChecker();List<Expression> expressions = new ArrayList<>();List<Fraction> answers = new ArrayList<>();// 生成不重复的表达式while (expressions.size() < count && attempts < maxAttempts) {attempts++;Expression expr = generator.generateExpression();if (!duplicateChecker.isDuplicate(expr)) {try {Fraction answer = expr.evaluate();// 检查答案是否为负数if (answer.compareTo(new Fraction(0)) >= 0) {expressions.add(expr);answers.add(answer);duplicateChecker.addExpression(expr);}} catch (Exception e) {// 跳过错误表达式}}}// 写入文件writeExercisesToFile(expressions, "Exercises.txt");writeAnswersToFile(answers, "Answers.txt");}
}
主程序实现了两种运行模式:生成题目和评分。在生成题目模式下,它会生成指定数量的不重复题目,并将题目和答案保存到文件中。
测试运行
题目与计算结果表格
题目 | 计算结果 |
---|---|
3'1/3 + 2 = | 5'1/3 |
5 - 3/4 = | 4'1/4 |
2/5 × 1'1/2 = | 3/5 |
4'2/3 ÷ 1/3 = | 14 |
(1/2 + 3/4) × 2/3 = | 5/6 |
0 ÷ 5/6 = | 0 |
3/4 × 2/5 × 5/6 = | 1/4 |
1/2 ÷ (1/3 ÷ 1/4) = | 3/8 |
2'1/4 - 1/2 + 3/8 = | 2'1/8 |
5/8 × 4/5 ÷ 2/3 = | 3/4 |
测试说明:
- 自动化简:所有结果均通过
Fraction
库将分数化简为最简形式(带分数与假分数转换符合数学习惯,如17/8
自动转为2'1/8
); - 手工验证:每道题目的计算结果均与分步手工计算(如带分数转假分数、通分、约分)完全一致;
- 格式兼容:判分模块支持识别带分数(如
3'1/3
)、假分数(如7/3
)、整数(如14
)等多种答案格式,并能正确判断等效答案(例如2'1/3
与7/3
视为同一正确结果)。
项目小结
成功之处
-
良好的面向对象设计:项目采用了清晰的类层次结构,遵循了面向对象的设计原则,代码结构清晰、易于维护。
-
完整的功能实现:成功实现了所有要求的功能,包括表达式生成、重复检测、分数计算、文件读写和评分功能。
-
健壮的错误处理:添加了全面的错误处理机制,使程序在各种异常情况下都能优雅地处理,不会轻易崩溃。
-
高效的重复检测:使用规范化形式的方法有效地检测了等价表达式,确保生成的题目不重复。
经验教训
-
性能优化的重要性:在生成大量题目的情况下,性能问题变得突出,需要提前考虑优化策略。
-
边界条件的处理:分数运算中有许多边界条件需要特别注意,如除零、负数结果、假分数化简等。
-
测试的重要性:全面的测试是确保程序正确性的关键,尤其是对于涉及数学计算的程序。
结对感受
通过这次结对项目,我们深刻体会到了团队合作的重要性。两个人可以相互讨论、相互补充,共同解决问题。在遇到困难时,另一个人的思路往往能提供新的视角,帮助突破瓶颈。
闪光点与建议
队友闪光点:
- 代码结构设计清晰,注重面向对象原则
- 对细节有很好的把控能力,尤其是在分数计算部分
- 测试意识强,编写了全面的测试用例
改进建议:
- 可以进一步优化性能,特别是在生成大量题目的情况下
- 可以增加更多的配置选项,让用户能够自定义题目的难度和类型
- 可以考虑添加图形用户界面,提高用户体验
总的来说,这次结对项目是一次非常有价值的经历,不仅完成了功能需求,还提高了我们的团队合作能力和编程水平。