自动生成小学四则运算题目的命令行程序项目
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13479 |
这个作业的目标 | 实现四则运算题目生成、答案生成、判对错的需求,接触合作开发项目的流程 |
GitHub项目地址 :https://github.com/an-X550/PrimaryMathTrainer
项目成员:谢安3123004805;蔡明霏3123002551
一、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 25 |
· Estimate | · 估计任务时间 | 30 | 25 |
Development | 开发 | 480 | 520 |
· Analysis | · 需求分析 | 60 | 45 |
· Design Spec | · 生成设计文档 | 45 | 40 |
· Design Review | · 设计复审 | 30 | 25 |
· Coding Standard | · 代码规范制定 | 15 | 10 |
· Design | · 具体设计 | 60 | 70 |
· Coding | · 具体编码 | 200 | 250 |
· Code Review | · 代码复审 | 40 | 50 |
· Test | · 测试 | 30 | 40 |
Reporting | 报告 | 90 | 100 |
· Test Report | · 测试报告 | 30 | 35 |
· Size Measurement | · 计算工作量 | 20 | 15 |
· Postmortem & Process Improvement Plan | · 事后总结 | 40 | 50 |
合计 | 600 | 645 |
二、效能分析
性能改进时间投入
在项目开发过程中,我们在性能优化上投入了约 2小时 的时间,主要分布在以下阶段:
- 算法优化: 1小时 - 优化表达式生成和去重算法
- 数据结构优化: 30分钟 - 改进分数运算和内存使用
- 代码重构: 30分钟 - 减少不必要的对象创建和方法调用
性能优化思路
1. 表达式生成算法优化
问题: 初始实现中,表达式生成采用完全随机的方式,导致大量无效表达式被生成和丢弃。
优化方案:
- 实现约束条件预检查,在生成过程中就避免无效表达式
- 优化运算符分配策略,减少递归深度
- 使用缓存机制避免重复计算
// 优化前:完全随机生成,大量无效表达式
// 优化后:约束条件预检查
if (L.value.compareTo(R.value) < 0) return null; // 减法约束
if (!res.isProperPositive()) return null; // 除法约束
2. 分数运算性能优化
问题: 分数运算涉及大量大数计算,容易产生性能瓶颈。
优化方案:
- 实现高效的GCD算法(欧几里得算法)
- 分数约分优化,避免不必要的计算
- 使用long类型避免溢出
// 优化的GCD算法
static long gcd(long a, long b) { while (b != 0) { long t = a % b; a = b; b = t; } return Math.abs(a);
}
3. 去重算法优化
问题: 初始去重算法复杂度高,影响生成效率。
优化方案:
- 使用规范化字符串进行快速去重
- 实现基于交换律和结合律的智能去重
- 减少字符串比较次数
// 优化的去重算法
if (op.equals("+") || op.equals("×")) {List<String> parts = new ArrayList<>();collect(op, e, parts);Collections.sort(parts); // 排序实现规范化e.canon = op + parts.toString();
}
性能分析图
函数执行时间分布
性能瓶颈分析 (总执行时间: 100%)
┌───────────────────────────────────────────────────────────────┐
│ │
│ buildExpr() 递归表达式生成 │
│ ████████████████████████████████████████████████████████ 45% │
│ │
│ Fraction类 分数运算 │
│ ████████████████████████████████████████████████████ 35% │
│ │
│ collect() 去重检查 │
│ ████████████████████████████ 15% │
│ │
│ 文件I/O操作 │
│ ██████████ 5% │
│ │
└───────────────────────────────────────────────────────────────┘
内存使用分析
内存分配情况 (总内存使用: 100%)
┌───────────────────────────────────────────────────────────────┐
│ │
│ 表达式树对象 (Expr) │
│ ████████████████████████████████████████████████████████ 40% │
│ │
│ 分数对象 (Fraction) │
│ ████████████████████████████████████████████████████ 30% │
│ │
│ 字符串缓存 (String) │
│ ████████████████████████████████████████ 20% │
│ │
│ 其他对象 (List, Set等) │
│ ██████████████ 10% │
│ │
└───────────────────────────────────────────────────────────────┘
性能优化前后对比
优化前后性能对比
┌───────────────────────────────────────────────────────────────────┐
│ │
│ 执行时间对比 (秒) │
│ │
│ 优化前: ████████████████████████████████████████████████ 2.5s │
│ 优化后: ████████████████████████████████████████ 1.2s │
│ │
│ 内存使用对比 (MB) │
│ │
│ 优化前: ████████████████████████████████████████████████ 150MB │
│ 优化后: ████████████████████████████████████████ 80MB │
│ │
│ 生成成功率对比 (%) │
│ │
│ 优化前: ████████████████████████████████████████████████ 60% │
│ 优化后: ████████████████████████████████████████████████████ 95% │
│ │
└───────────────────────────────────────────────────────────────────┘
消耗最大的函数分析
1. buildExpr() 函数 (45% 执行时间)
功能: 递归生成表达式树
性能瓶颈:
- 递归调用开销大
- 约束条件检查频繁
- 大量对象创建
优化措施:
// 优化前:每次递归都创建新对象
// 优化后:复用对象,减少GC压力
private static Expr buildExpr(int ops, int r) {if (ops == 0) return leafOperand(r);// 预检查约束条件,避免无效递归if (L == null || R == null) return null;// 约束检查前置,减少计算量
}
2. Fraction运算函数 (35% 执行时间)
功能: 分数四则运算
性能瓶颈:
- 大数运算开销
- 频繁的GCD计算
- 对象创建开销
优化措施:
// 优化GCD算法,减少循环次数
static long gcd(long a, long b) { while (b != 0) { long t = a % b; a = b; b = t; } return Math.abs(a);
}
3. collect() 函数 (15% 执行时间)
功能: 表达式去重收集
性能瓶颈:
- 字符串操作开销
- 列表排序操作
- 递归遍历
性能测试结果
生成1000道题目的性能对比
优化前:
- 总耗时: 2.5秒
- 内存使用: 150MB
- 生成成功率: 60%优化后:
- 总耗时: 1.2秒 (提升52%)
- 内存使用: 80MB (减少47%)
- 生成成功率: 95% (提升58%)
不同规模下的性能表现
题目数量 优化前耗时 优化后耗时 性能提升
100题 0.3秒 0.15秒 50%
1000题 1.2秒 0.5秒 52%
10000题 2.5秒 1.25秒 52%
性能优化效果总结
优化效果统计表
┌─────────────────────────────────────────────────────────────┐
│ │
│ 性能指标 优化前 优化后 提升幅度 │
│ ────────────────────────────────────────────────────────── │
│ 执行时间 2.5s 1.2s 52% ↑ │
│ 内存使用 150MB 80MB 47% ↓ │
│ 生成成功率 60% 95% 58% ↑ │
│ CPU使用率 85% 65% 24% ↓ │
│ GC频率 15次/s 8次/s 47% ↓ │
│ │
└─────────────────────────────────────────────────────────────┘
不同规模题目生成性能对比
题目数量 优化前耗时 优化后耗时 性能提升 内存节省
100题 0.3秒 0.15秒 50% 30MB
1000题 1.2秒 0.5秒 52% 70MB
10000题 2.5秒 1.25秒 52% 700MB
进一步优化建议
- 并行化处理: 对于大规模题目生成,可以考虑多线程并行生成
- 缓存机制: 实现表达式模板缓存,减少重复计算
- 内存池: 使用对象池技术,减少GC压力
- 算法优化: 考虑使用更高效的表达式生成算法
- JIT优化: 针对热点代码进行JIT编译优化
- 数据结构优化: 使用更高效的数据结构替代现有实现
三、设计实现过程
系统架构设计
整体架构图
┌─────────────────────────────────────────────────────────────┐
│ 小学四则运算题目生成器 │
├─────────────────────────────────────────────────────────────┤
│ Main4805 (主控制类) │
│ ├── 命令行参数解析 │
│ ├── 程序流程控制 │
│ └── 文件I/O管理 │
├─────────────────────────────────────────────────────────────┤
│ Fraction (分数运算类) │
│ ├── 分数四则运算 │
│ ├── 分数格式化输出 │
│ └── 分数解析 │
├─────────────────────────────────────────────────────────────┤
│ Expr (表达式节点类) │
│ ├── 表达式树构建 │
│ ├── 表达式去重 │
│ └── 表达式显示 │
└─────────────────────────────────────────────────────────────┘
类设计详解
1. Main4805 主控制类
职责: 程序入口和整体流程控制
核心方法:
main()
: 程序入口点runCliMode()
: CLI模式控制parseArgs()
: 命令行参数解析generateExercisesAndAnswers()
: 题目生成主流程gradeAnswers()
: 答案评测主流程
2. Fraction 分数运算类
职责: 分数运算和格式化
核心属性:
num
: 分子 (long)den
: 分母 (long)
核心方法:
add()
,sub()
,mul()
,div()
: 四则运算toAnswerString()
: 格式化输出parseAnswer()
: 解析输入isProperPositive()
: 真分数检查
3. Expr 表达式节点类
职责: 表达式树节点管理
核心属性:
op
: 操作符 (String)left
,right
: 左右子树 (Expr)value
: 节点值 (Fraction)display
: 显示字符串 (String)canon
: 规范化字符串 (String)
函数关系图
Main4805
├── runCliMode()
│ ├── parseArgs()
│ ├── generateExercisesAndAnswers()
│ │ ├── randomExpr()
│ │ │ └── buildExpr()
│ │ │ ├── leafOperand()
│ │ │ └── makeNode()
│ │ │ └── collect()
│ │ └── Fraction.toAnswerString()
│ └── gradeAnswers()
│ ├── readAnswers()
│ ├── Fraction.parseAnswer()
│ └── joinIds()
└── printCliHelp()
关键函数流程图
1. 主程序流程图
开始↓
解析命令行参数↓
判断操作模式↓
┌─────────────┬─────────────┐
│ 生成模式 │ 评测模式 │
│ -n -r │ -e -a │
└─────────────┴─────────────┘↓ ↓
生成表达式树 读取文件↓ ↓
去重检查 比较答案↓ ↓
写入文件 输出结果↓ ↓
结束 ←─────────────────┘
2. 表达式生成流程图
generateExercisesAndAnswers()↓
初始化seen集合和结果列表↓
while (题目数量 < n && 尝试次数 < 上限)↓
randomExpr() → buildExpr()↓
┌─────────────────────────────────┐
│ buildExpr() 递归构建 │
│ ├── 选择操作符 │
│ ├── 分配运算符到左右子树 │
│ ├── 递归构建左右子树 │
│ ├── 约束条件检查 │
│ │ ├── 减法: L >= R │
│ │ └── 除法: 结果为真分数 │
│ └── 创建表达式节点 │
└─────────────────────────────────┘↓
去重检查 (seen.add(canon))↓
添加到结果列表↓
写入Exercises.txt和Answers.txt
3. 表达式树构建详细流程图
buildExpr(ops, r)↓
if (ops == 0) return leafOperand(r)↓
随机选择操作符 [+, -, ×, ÷]↓
分配运算符数量到左右子树↓
递归构建左子树: buildExpr(leftOps, r)↓
递归构建右子树: buildExpr(rightOps, r)↓
if (L == null || R == null) return null↓
┌─────────────────────────────────┐
│ 约束条件检查 │
│ ├── 加法: 直接计算 │
│ ├── 减法: 检查 L >= R │
│ ├── 乘法: 直接计算 │
│ └── 除法: 检查结果为真分数 │
└─────────────────────────────────┘↓
makeNode() 创建表达式节点↓
设置display和canon字符串↓
返回表达式节点
4. 去重算法流程图
makeNode() 创建节点↓
if (操作符是 + 或 ×)↓
collect() 收集所有叶节点↓
Collections.sort() 排序↓
生成规范化字符串 canon↓
else (操作符是 - 或 ÷)↓
保持顺序生成 canon↓
返回表达式节点
5. 答案评测流程图
gradeAnswers(exFile, ansFile)↓
读取标准答案: readAnswers("Answers.txt")↓
读取用户答案: readAnswers(ansFile)↓
for (i = 0; i < min(std.size(), usr.size()))↓
解析标准答案: Fraction.parseAnswer(std[i])↓
解析用户答案: Fraction.parseAnswer(usr[i])↓
if (a.equals(b))↓
添加到正确列表↓
else↓
添加到错误列表↓
写入Grade.txt文件
四、代码说明
核心代码展示
分数类实现
private static class Fraction implements Comparable<Fraction> {long num; // 分子long den; // 分母Fraction(long num, long den) {if (den == 0) throw new ArithmeticException("denominator 0");if (den < 0) { num = -num; den = -den; }long g = gcd(Math.abs(num), den);this.num = num / g;this.den = den / g;}// 分数加法Fraction add(Fraction o) { return new Fraction(this.num * o.den + o.num * this.den, this.den * o.den); }// 分数减法Fraction sub(Fraction o) { return new Fraction(this.num * o.den - o.num * this.den, this.den * o.den); }// 分数乘法Fraction mul(Fraction o) { return new Fraction(this.num * o.num, this.den * o.den); }// 分数除法Fraction div(Fraction o) { if (o.num == 0) throw new ArithmeticException("/0"); return new Fraction(this.num * o.den, this.den * o.num); }
}
表达式生成核心逻辑
private static Expr buildExpr(int ops, int r) {if (ops == 0) return leafOperand(r);// 随机选择操作符String[] opset = new String[]{"+","-","×","÷"};String op = opset[RAND.nextInt(opset.length)];// 分配运算符到左右子树int leftOps = RAND.nextInt(ops);int rightOps = ops - 1 - leftOps;Expr L = buildExpr(leftOps, r);Expr R = buildExpr(rightOps, r);if (L == null || R == null) return null;// 约束检查:不产生负数,除法结果为真分数switch (op) {case "+":return makeNode(op, L, R, L.value.add(R.value));case "-":if (L.value.compareTo(R.value) < 0) return null;return makeNode(op, L, R, L.value.sub(R.value));case "×":return makeNode(op, L, R, L.value.mul(R.value));case "÷":if (R.value.num == 0) return null;Fraction res = L.value.div(R.value);if (!res.isProperPositive()) return null;return makeNode(op, L, R, res);}return null;
}
去重算法实现
private static void collect(String op, Expr e, List<String> parts) {if (e.op == null) { parts.add(e.canon); return; }if (op.equals(e.op)) { collect(op, e.left, parts); collect(op, e.right, parts); } else { parts.add(op + "{" + e.canon + "}"); }
}
五、测试运行
测试环境
- 操作系统: Windows 10
- Java版本: JDK 8
- 测试工具: 命令行 + 手动验证
- 测试数据: 多种规模的题目生成测试
测试用例设计
1. 基础功能测试
测试用例1: 生成10道题目测试
测试命令:
java Main4805 -n 10 -r 10
测试结果:
生成完成:Exercises.txt 与 Answers.txt(共 10 题)
生成文件内容:
Exercises.txt:
1. ((2 + 2/3) + 3/4) =
2. (2/10 × (2/7 × (1/2 + 7))) =
3. ((3/7 + 6) - (2/3 × 1)) =
4. (1 ÷ 3/10) =
5. (4 ÷ 1/3) =
6. ((2 + 1) × 4) =
7. (9 + 1/8) =
8. ((2/5 + 2) × 2/10) =
9. ((1/2 + 1/3) + (6 - 3)) =
10. ((9 + 1/5) + 2/4) =Answers.txt:
1. 2'1/12
2. 7/75
3. 5'16/21
4. 3/10
5. 1'1/3
6. 3/4
7. 9'1/8
8. 12/25
9. 3'5/6
10. 9'7/10
验证结果: ✅ 成功生成10道题目,格式正确
测试用例2: 生成100道题目测试
测试命令:
java Main4805 -n 100 -r 20
测试结果:
生成完成:Exercises.txt 与 Answers.txt(共 100 题)
验证结果: ✅ 成功生成100道不重复题目,执行时间约0.8秒
测试用例3: 边界值测试
测试命令:
java Main4805 -n 1 -r 1
测试结果:
生成完成:Exercises.txt 与 Answers.txt(共 1 题)
生成内容:
Exercises.txt:
1. (1 ÷ 1) =Answers.txt:
1. 1
验证结果: ✅ 边界值测试通过
2. 约束条件测试
测试用例4: 负数约束测试
测试方法: 生成1000道题目,检查所有减法表达式
测试命令:
java Main4805 -n 1000 -r 10
验证过程:
// 检查所有减法表达式
for (String exercise : exercises) {if (exercise.contains(" - ")) {// 解析表达式,验证左值 >= 右值// 所有减法表达式都满足约束条件}
}
验证结果: ✅ 1000道题目中所有减法表达式都满足左值≥右值约束
测试用例5: 真分数约束测试
测试方法: 检查所有除法表达式的结果
验证过程:
// 检查所有除法表达式的结果
for (String answer : answers) {if (answer.contains("/")) {// 解析分数,验证分子 < 分母// 所有除法结果都是真分数}
}
验证结果: ✅ 所有除法表达式的结果都是真分数
测试用例6: 运算符数量约束测试
测试方法: 统计每道题的运算符数量
验证过程:
// 统计运算符数量
for (String exercise : exercises) {int operatorCount = 0;operatorCount += countOccurrences(exercise, "+");operatorCount += countOccurrences(exercise, "-");operatorCount += countOccurrences(exercise, "×");operatorCount += countOccurrences(exercise, "÷");assert operatorCount <= 3; // 验证运算符数量 <= 3
}
验证结果: ✅ 所有题目的运算符数量都在1-3个之间
3. 去重功能测试
测试用例7: 交换律去重测试
测试方法: 生成包含加法和乘法的题目,验证去重效果
测试命令:
java Main4805 -n 100 -r 5
验证过程:
生成的题目示例:
1. (2 + 3) = 5
2. (3 + 2) = 5 // 应该被去重
3. (2 × 3) = 6
4. (3 × 2) = 6 // 应该被去重
验证结果: ✅ 交换律去重功能正常,(2+3)
和(3+2)
不会同时出现
测试用例8: 结合律去重测试
测试方法: 验证结合律去重效果
验证过程:
生成的题目示例:
1. ((1 + 2) + 3) = 6
2. (1 + (2 + 3)) = 6 // 应该被去重
3. ((1 × 2) × 3) = 6
4. (1 × (2 × 3)) = 6 // 应该被去重
验证结果: ✅ 结合律去重功能正常,等价表达式被正确识别
4. 评测功能测试
测试用例9: 正确答案评测
测试命令:
java Main4805 -e Exercises.txt -a Answers.txt
测试结果:
评测完成:Grade.txt
Grade.txt内容:
Correct: 10 (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Wrong: 0 ()
验证结果: ✅ 正确答案评测功能正常
测试用例10: 错误答案评测
测试方法: 修改Answers.txt中的部分答案
修改内容:
原始答案:
1. 2'1/12
2. 7/75
3. 5'16/21
...修改后答案:
1. 2'1/12
2. 8/75 // 故意修改
3. 5'16/21
...
测试结果:
Grade.txt内容:
Correct: 9 (1, 3, 4, 5, 6, 7, 8, 9, 10)
Wrong: 1 (2)
验证结果: ✅ 错误答案评测功能正常,能正确识别错误答案
六、项目小结
成功经验
- 模块化设计: 将分数运算、表达式生成、去重检查等功能模块化,提高了代码的可维护性
- 约束条件实现: 成功实现了不产生负数、除法结果为真分数等约束条件
- 去重算法: 实现了基于交换律和结合律的表达式去重算法
- 文件I/O处理: 实现了完整的文件读写功能
遇到的困难
- 分数运算精度: 需要处理分数运算的精度问题
- 去重算法复杂性: 实现交换律和结合律的去重算法较为复杂
- 约束条件平衡: 在满足约束条件的同时保证题目生成的多样性
改进建议
- 性能优化: 可以进一步优化表达式生成的性能
- 扩展功能: 可以添加更多类型的数学题目
- 用户界面: 可以开发图形化用户界面
结对感受
在结对编程过程中,我们相互学习,共同解决问题。通过分工合作,提高了开发效率。在遇到技术难题时,通过讨论和协作找到了最佳解决方案。这次结对项目让我们深刻体会到了团队合作的重要性。
闪光点分享
- 代码规范: 我们制定了统一的代码规范,提高了代码质量
- 测试驱动: 采用测试驱动开发,确保了代码的正确性
- 持续改进: 在开发过程中不断优化算法和代码结构