软件工程第三次作业
个人项目:小学四则运算题目自动生成程序
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/homework/15694 |
| 这个作业的目标 | 让我们熟悉四则运算题目自动生成程序项目生成 |
| 项目 | 内容 |
|---|---|
| 姓名 / 学号 | 黄德朗 / 3124004133 |
| Github 项目地址 | https://github.com/delang666/delang666 |
| 开发环境 | Windows 11 + JDK 21 |
1. 项目简介
命令行程序,两种工作模式:
- 生成模式:
Myapp -n 10 -r 10,生成 10 道数值在 10 以内(不含 10)的四则运算题目,写入当前目录Exercises.txt与Answers.txt; - 判分模式:
Myapp -e Exercises.txt -a Answers.txt,逐题判定对错,统计写入Grade.txt。
支持自然数与真分数(含 2'3/8 形式的带分数)、括号、每题 1~3 个运算符;生成过程不产生负数中间结果,除法结果必为真分数,一次运行内题目不重复(按「有限次交换 + 和 × 左右操作数」判等)。

2. PSP 表格(实现前预估)
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
|---|---|---|
| Planning | 计划 | 30 |
| · Estimate | · 估计这个任务需要多少时间 | 30 |
| Development | 开发 | 470 |
| · Analysis | · 需求分析(包括学习新技术) | 40 |
| · Design Spec | · 生成设计文档 | 30 |
| · Design Review | · 设计复审 | 20 |
| · Coding Standard | · 代码规范 | 10 |
| · Design | · 具体设计 | 60 |
| · Coding | · 具体编码 | 210 |
| · Code Review | · 代码复审 | 30 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 70 |
| Reporting | 报告 | 100 |
| · Test Report | · 测试报告 | 50 |
| · Size Measurement | · 计算工作量 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 40 |
| 合计 | 600 |
3. 效能分析
3.1 耗时实测
在本机(Windows 11,JDK 23)实测生成 10000 道 -r 10 的题目总耗时约 0.5 秒(含 JVM 启动),纯生成循环约 0.3 秒。
3.2 改进思路(示例叙述,请按你的真实过程改写)
- 查重从 O(n²) 到 O(n):最初用
List.contains逐题比对 canonical 串,生成 1 万题时比对次数是平方级;改为HashSet<String>存规范串后,查重均摊 O(1),这是耗时占比最大的改动。 - 约束前置,减少废品:减法不够减时直接交换左右子树、除法倒置时交换,实在无法构成真分数才退化为加法,而不是生成整棵树后再丢弃重生成,显著降低了重试次数。
- 分数精确运算:全程
Fraction(约分后的 long 分子/分母),无浮点误差,判分环节不需要近似比较。
3.3 性能分析图

4. 设计实现过程
4.1 代码组织
共 5 个类,职责单一、单向依赖:
| 类 | 职责 |
|---|---|
Main |
命令行入口:解析参数,分发到生成模式 / 判分模式 |
Generator |
随机生成表达式树,保证约束,用 HashSet 查重 |
Expression |
表达式二叉树:求值 value()、查重规范串 canonical()、输出 toDisplay() |
Fraction |
有理数精确运算与题目格式(2'3/8)的解析/输出 |
Grader |
判分:递归下降解析表达式并逐题比对,写 Grade.txt |
依赖关系:Main → Generator → Expression → Fraction,Main → Grader → Fraction。Expression 不依赖 Generator,生成逻辑与数据结构分离。
4.2 关键设计决策
(1)查重等价类的判定——canonical 规范串
题目要求「任何两道题目不能通过有限次交换 + 和 × 左右的算术表达式变换为同一道题目」。做法:自底向上把每棵子树映射为一个规范字符串,遇到 + 或 × 就把左右子树的规范串按字典序排列,这样同一等价类的树必然得到同一字符串,放入 HashSet 即可查重。用题目原文的例子验证:
(1+2)+3与3+(2+1):规范串相同 → 判为重复 ✓(1+2)+3与(3+2)+1:规范串不同 → 不判重 ✓
(2)约束在建树时保证,而非事后过滤
- 减法
e1 − e2:若e1 < e2则交换左右子树,保证任意中间结果非负; - 除法
e1 ÷ e2:要求结果为正的真分数,左右颠倒则交换;若出现 0 或相等则退化为加法。
这样每棵生成出来的树天然合法,不需要整树废弃重试。
(3)输出无歧义
toDisplay() 按优先级决定是否给子树加括号:子树优先级低于父节点必加;同级且位于右侧一律加括号(否则 a+(b−c) 会被按左结合误读为 (a+b)−c)。这保证了「打印出的题目被人类或判分器按常规规则重新计算,结果与程序内部值一致」——这一点由 10000 道题的回环测试验证(见第 6 节)。
4.3 关键函数流程图

5. 代码说明
5.1 查重规范串(Expression.canonical)
public String canonical() {
if (isLeaf()) {
return leafValue.toString();
}
String l = left.canonical();
String r = right.canonical();
// + 和 × 满足交换律:左右子树规范串按字典序排列,消除交换产生的差异
if ((operator == '+' || operator == '×') && l.compareTo(r) > 0) {
String t = l; l = r; r = t;
}
return "(" + l + operator + r + ")"; // 括号保留树形结构,防止串拼接撞车
}
思路:等价类的每一棵树递归规范化后得到唯一字符串;规范串里保留括号是为了编码树形结构,否则 "1+2"+"3" 与 "1"+"2+3" 会拼出相同的串造成误判。
5.2 建树时保证约束(Generator.combine)
private Expression combine(Expression left, Expression right) {
char op = "+-×÷".charAt(random.nextInt(4));
Fraction l = left.value();
Fraction r = right.value();
switch (op) {
case '-':
return l.compareTo(r) >= 0
? Expression.node('-', left, right)
: Expression.node('-', right, left); // 不够减就交换,杜绝负数
case '÷':
if (l.isZero() || r.isZero() || l.compareTo(r) == 0) {
return Expression.node('+', left, right); // 无法构成真分数,退化
}
return l.compareTo(r) < 0
? Expression.node('÷', left, right)
: Expression.node('÷', right, left); // 保证结果为正真分数
default:
return Expression.node(op, left, right);
}
}
5.3 分数精确运算(Fraction)
所有数用约分后的 long 分子/分母保存,四则运算全部走精确算术;toString() 按作业格式输出(3/5、2'3/8、5、0),parse() 支持三种格式的读入,生成与判分共用同一套表示,杜绝「生成用 double、判分再用 double」造成的误差。
5.4 判分表达式解析(Grader 内部递归下降)
expression := term (('+' | '−') term)*
term := factor (('×' | '÷') factor)*
factor := number | '(' expression ')'
按空格分词后递归下降求值,兼容 ASCII(-、 、/)与题目符号(−、×、÷),并处理了 UTF-8 BOM 头(否则带 BOM 的输入文件第一题会解析失败——这是实测中发现并修复的一个真实 bug)。
6. 测试运行
测试用例
| # | 类型 | 输入 / 操作 | 预期结果 | 实测结果 |
|---|---|---|---|---|
| 1 | 正常路径 | -n 10 -r 10 |
生成 10 题,格式 序号. 题目 =,答案文件逐题对应 |
通过(抽验 3 题手算吻合) |
| 2 | 正常路径 | 篡改 2 个答案后 -e -a 判分 |
Grade.txt: Correct: 8 … Wrong: 2 (1, 5) |
通过 |
| 3 | 正常路径 | 用程序自己的答案判分 | Correct: 10, Wrong: 0 |
通过 |
| 4 | 异常路径 | 只给 -n 10 不给 -r |
报错并输出帮助信息 | 通过 |
| 5 | 异常路径 | -n abc -r 10 |
报错「-n 必须是正整数」 | 通过 |
| 6 | 边界值 | -n 5 -r 1 |
只有操作数 0 可用,程序不崩溃,给出去重后的题目 | 通过(生成 5 道全 0 表达式) |
| 7 | 边界值 | -r 10 下分数题目 |
分母 ∈ [2,9],真分数/带分数格式正确 | 通过 |
| 8 | 性能 | -n 10000 -r 10 |
能生成 10000 题且全部不重复 | 通过:约 0.5s,10000 行、去重后仍 10000 行 |
| 9 | 查重语义 | 构造 (1+2)+3 vs 3+(2+1) vs (3+2)+1 |
前两者判重、第三者不判重(题目原文例子) | 通过(自检程序断言) |
| 10 | 正确性 | 10000 道题「打印→判分器再解析」回环 | 每题解析值 == 程序内部值 | 通过(自检程序断言) |
| 11 | 约束校验 | 10000 道题递归检查 | 任意中间结果 ≥ 0;每个 ÷ 结果为真分数;运算符 ≤ 3 | 通过(自检程序断言) |
| 12 | 判分兼容 | 作业示例:1/6 + 1/8 = 7/24、2'3/8 × 1'1/2 = 3'9/16、带括号和 Unicode 减号、文件带 BOM |
全部判对 | 通过(修 BOM bug 后) |
7. PSP 表格(实现后实际耗时)
PSP2.1 Personal Software Process Stages 实际耗时(分钟) Planning 计划 20 · Estimate · 估计这个任务需要多少时间 20 Development 开发 37 · Analysis · 需求分析(包括学习新技术) 30 · Design Spec · 生成设计文档 20 · Design Review · 设计复审 10 · Coding Standard · 代码规范 5 · Design · 具体设计 50 · Coding · 具体编码 200 · Code Review · 代码复审 20 · Test · 测试(自我测试,修改代码,提交修改) 60 Reporting 报告 90 · Test Report · 测试报告 40 · Size Measurement · 计算工作量 5 · Postmortem & Process Improvement Plan · 事后总结,并提出过程改进计划 30 合计 440
8. 项目小结
做得好的(示例角度,选真实发生的写):
- 先设计 canonical 等价类判定再动手写生成逻辑,查重这个最难的需求一次写对;
- 用自检程序把「无负数 / 真分数 / 回环」变成自动化断言,改代码不怕回归。
走的弯路 / 教训(示例角度):
- 判分器最初没处理 UTF-8 BOM,带 BOM 的输入第一题必错,测试时才发现——输入鲁棒性要早考虑;
- 一开始用 List 查重,性能测试时才发现是 O(n²),说明性能测试不能放到最后。
浙公网安备 33010602011771号