小学四则运算题目生成器

GitHub 项目地址:https://github.com/linyanzhi591-afk/arithmetic-generator

结对项目:小学四则运算题目生成器

项目成员:肖琳瀚(3124004184)、陈康泓(3124004159)

一、项目说明

本次结对项目使用 Java 17 实现一个小学四则运算命令行程序。程序能够生成包含自然数、分数和带分数的四则运算题,自动计算标准答案,还能读取题目文件与答案文件完成批改并统计正确、错误题号。

项目采用 Maven 管理构建,入口为 com.everyday.arithmetic.Main,构建产物为 target/Myapp.jar。生成题目时运行:

java -jar target/Myapp.jar -n 10 -r 10

程序会在当前目录生成 Exercises.txtAnswers.txt。其中 -n 控制题目数量,可省略并默认为 10;-r 控制数值范围,必须提供。

批改已有答案时运行:

java -jar target/Myapp.jar -e Exercises.txt -a Answers.txt

批改结果保存在当前目录的 Grade.txt。项目仅在构建测试时使用 JUnit 5,正式运行不需要联网或额外依赖。

二、PSP2.1

开发前先按模块估算工作量,开发完成后再根据结对过程中的任务记录填写实际耗时。实际总耗时比预估多 40 分钟,主要偏差来自表达式去重规则的讨论、随机边界缺陷的定位以及一万道题的性能验证。

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 15 20
· Estimate · 估计任务需要多少时间 15 20
Development 开发 285 310
· Analysis · 需求分析(包括学习新技术) 30 35
· Design Spec · 生成设计文档 20 25
· Design Review · 设计复审 10 10
· Coding Standard · 代码规范 10 10
· Design · 具体设计 35 40
· Coding · 具体编码 100 110
· Code Review · 代码复审 25 25
· Test · 测试、修改代码、提交修改 55 55
Reporting 报告 60 70
· Test Report · 测试报告 20 25
· Size Measurement · 计算工作量 10 10
· Postmortem & Process Improvement Plan · 事后总结及改进计划 30 35
合计 360 400

三、需求分析

程序有两个工作模式:生成模式接收 -n-r,输出题目与标准答案;批改模式接收 -e-a,输出正确和错误题号。实现中的难点不是随机选择四个运算符,而是同时维护以下不变量:

  1. 中间减法结果不能为负数;
  2. 每一个除法子式的结果都必须是真分数;
  3. 每题最多三个运算符;
  4. 分数必须精确计算和约分;
  5. 交换任意加法或乘法结点的左右子树后相同的题目必须判重;
  6. 一万题规模下仍应快速结束,失败时也不能无限重试。

四、设计与实现过程

程序使用表达式树作为核心模型。叶结点保存有理数,非叶结点保存运算符以及左右子树。生成、求值、输出括号和去重都在同一棵树上完成,避免用字符串拼接后再猜测表达式含义。

flowchart LR A[Main 参数解析] --> B{工作模式} B -->|生成| C[ExpressionGenerator] C --> D[Expression 表达式树] D --> E[Rational 精确求值] D --> F[规范化键去重] D --> G[ExerciseService 写题目和答案] B -->|批改| H[ExerciseParser 解析题目] H --> E E --> I[ExerciseService 比较答案] I --> J[Grade.txt]

4.1 类的职责

职责
Main 解析参数、检查模式、输出帮助信息
ExerciseService 生成文件、读取编号行、批改并输出统计
ExpressionGenerator 随机创建表达式树并维持约束
ExpressionParser 按优先级和左结合规则解析已有题目
Expression 表达式的求值、显示、去重键接口
NumberExpression 数字叶结点
BinaryExpression 二元运算结点,负责计算和括号输出
Rational 基于 BigInteger 的不可变有理数
Operator 四种运算符的符号、优先级和交换性

4.2 生成流程

先随机确定运算符数量,再随机划分左右子树需要的运算符数量。创建减法结点时,如果左值小于右值就交换两棵子树;创建除法结点时,拒绝除数为零或商不是真分数的候选。候选通过约束检查后生成规范化键,并放入 HashSet 判重。

随机运算符数量(1~3)
        ↓
递归生成左右子树
        ↓
检查减法、除法约束 ──失败──→ 放弃本次候选
        ↓成功
生成交换规范化键 ──已存在──→ 放弃本次候选
        ↓新题
加入结果列表,直到达到 -n

五、关键代码说明

5.1 精确分数

有理数的分子、分母使用 BigInteger,构造时统一分母符号并用最大公约数约分。因此 1/6 + 1/8 不经过浮点数,能精确得到 7/24。输出时通过带余除法将假分数格式化为 2’3/8

public Rational add(Rational other) {
    return new Rational(
        numerator.multiply(other.denominator)
            .add(other.numerator.multiply(denominator)),
        denominator.multiply(other.denominator));
}

5.2 中间结果约束

约束不是只检查整道题的最终答案,而是在每次构造二元结点时检查。因此表达式树中的所有减法、除法子式都满足要求。

if (operator == Operator.SUBTRACT
        && left.value().compareTo(right.value()) < 0) {
    Expression temporary = left;
    left = right;
    right = temporary;
}
if (operator == Operator.DIVIDE) {
    if (right.value().isZero()) return null;
    if (!left.value().divide(right.value()).isProperFraction()) return null;
}

5.3 去重规则

每个结点递归生成结构键。只有 +× 结点会把两个子键按字典序排序,减法和除法仍保留方向。这恰好对应题目所说的“有限次交换 + 和 × 左右表达式”,但不会错误地把所有同值表达式判成重复。

例如:

  • 23 + 4545 + 23 的键相同;
  • 3 + (2 + 1)1 + 2 + 3 的键相同;
  • 1 + 2 + 33 + 2 + 1 的树形不同,键也不同。
if (operator.isCommutative() && leftKey.compareTo(rightKey) > 0) {
    String temporary = leftKey;
    leftKey = rightKey;
    rightKey = temporary;
}
return operator.name() + "(" + leftKey + "," + rightKey + ")";

5.4 括号输出

输出表达式时比较父子运算符优先级。右子树与父结点优先级相同时也保留括号,以确保再次按左结合规则解析时得到原来的树。例如右嵌套加法必须输出为 3 + (2 + 1)

六、效能分析与改进

性能工作主要围绕一万道题的生成与去重。最初如果把每道新题和已有题目逐一比较,去重接近 O(n²);改为生成规范化字符串并存入 HashSet 后,平均查重成本降为 O(1)。此外,表达式结点在构造时缓存计算值,后续约束检查和答案输出不用重复遍历求值。分数采用不可变对象,便于安全复用。

在 Windows 11、Java 21、-r 10 环境下,每组独立运行 5 次,测量从 JVM 启动到两个文件写完的端到端耗时:

题目数 5 次平均耗时
100 166.15 ms
1,000 199.90 ms
10,000 332.08 ms

[题目生成性能]

performance

短任务中 JVM 启动和文件创建是明显的固定成本;题量增加 100 倍时,总耗时只约增加到 2 倍。生成阶段最频繁执行的函数是 ExpressionGenerator.createExpression,它负责递归建树和候选拒绝;去重阶段的热点是各结点的 canonicalKey。当前结果已满足一万题需求,不需要引入并发写文件等额外复杂度。

性能分析与优化实际投入 35 分钟,包括分析去重复杂度、使用不同题量进行五轮端到端计时、检查一万题输出规模并绘制性能图。

七、测试运行

项目使用 JUnit 5 编写 14 个自动化测试。下面列出至少 10 个核心用例:

编号 测试输入或场景 期望结果 覆盖目的
1 构造 2/4 输出 1/2 自动约分
2 构造 19/8 输出 2’3/8 带分数格式
3 1/6 + 1/8 7/24 精确分数计算
4 解析 2'3/82’3/8 都等于 19/8 两种撇号兼容性
5 构造 1/0 抛出异常 非法分母
6 1 + 2 × 3 7 运算优先级
7 (1 + 2) × 3 9 括号优先级
8 23 + 4545 + 23 去重键相同 加法交换去重
9 作业给出的三组连加表达式 前两组重复,第三组不重复 精确实现题意
10 固定随机种子生成 1,000 道题 无重复,运算符不超过 3,子式约束均成立 生成器综合属性
11 生成 10 道题 两个文件均为 10 行 文件输出
12 答案含正确、错误和缺失项 分别进入正确、错误编号 批改逻辑
13 生成模式缺少 -r 返回错误并显示帮助 参数校验
14 写入批改结果 符合 Correct/Wrong 指定格式 输出格式

运行命令:

mvn test

测试结果:Tests run: 14, Failures: 0, Errors: 0, Skipped: 0。另进行了系统验收:生成 10,000 道题后用 Answers.txt 回批,得到 10,000 道正确、0 道错误;Exercises.txtAnswers.txt 均为 10,000 行。还单独验证了 -r 1,程序可以只使用数字 0 生成结构不同的不重复题目。

这些测试同时覆盖了单元级计算、树结构规则、随机生成的整体不变量和端到端文件流程。固定随机种子的属性测试确保问题能够稳定复现,10,000 道题验收则验证规模要求。

八、GitHub 版本管理

项目在本地建立独立 Git 仓库,并按照功能阶段提交代码。主要阶段提交如下:

提交 提交信息 主要内容
38eea03 实现四则运算生成与批改核心功能 完成表达式树、精确有理数、题目生成、文件输出和批改流程
0013d52 补充自动化测试与运行说明 增加 14 个 JUnit 测试及 README
e1b45bb 添加项目博客与性能分析资料 增加博客初稿与性能图
7156d23 补全博客成员信息与结对总结 补充成员、PSP、结对感受和互评

提交按“核心功能—测试文档—博客资料”划分,每个阶段都保留独立记录,便于通过差异查看开发过程。项目上传后,博客首行链接指向 GitHub 仓库,读者可以从提交历史核对增量修改。

九、项目小结

本项目最重要的设计决策是先建立表达式树,再处理随机生成、精确求值、括号和去重。若直接操作字符串,减法和除法的中间约束、左结合规则以及交换去重很容易相互冲突。用统一的数据模型后,每条业务规则都能落在明确的类和函数中。

开发过程中,自动化测试确实发现过一次递归边界错误:内部除法候选被拒绝后返回空值,父结点没有立即放弃该候选。增加空值向上传递后问题解决。这说明随机算法也必须使用固定种子测试,否则缺陷可能只在偶然输入下出现。

结对感受

这次结对开发让我们体会到,结对并不是简单地把任务拆成两半,而是让设计、实现和验证形成相互反馈。前期我们共同梳理题意,特别讨论了“交换加法和乘法左右表达式”与普通数学等价之间的区别;实现阶段由肖琳瀚重点负责表达式树、精确分数和命令行流程,陈康泓重点负责测试用例、边界检查、性能数据与文档整理,两人通过代码复审共同确认关键逻辑。

合作中最有价值的一次讨论是去重方案。最初直观方案是比较表达式字符串,但它无法可靠处理子树交换;随后我们改为对表达式树递归生成规范化键。测试阶段,固定随机种子的用例暴露了非法除法子式向父结点传播时的空值问题,我们一起定位调用链并补上候选作废逻辑。相比各自独立完成模块,这种即时复审减少了隐藏错误,也让两个人都能说明完整设计,而不只了解自己编写的部分。

我们也发现结对开发需要控制沟通粒度:简单格式问题可以直接按约定处理,涉及题意和数据结构的选择则应先取得一致。后续合作中会更早建立验收清单,并在每个阶段提交后立即记录 PSP 时间,避免最后集中回忆造成偏差。

对同伴的闪光点与建议

  • 肖琳瀚对陈康泓:陈康泓在测试和边界条件方面很细致,主动覆盖了 -r 1、缺失答案、分数约分和一万题生成等场景,也能从失败用例快速缩小问题范围。建议后续在实现开始前就把测试矩阵写出来,让这些边界更早影响接口设计。
  • 陈康泓对肖琳瀚:肖琳瀚能够把复杂的题意转化为清晰的表达式树模型,尤其是规范化键的设计准确保留了题目规定的交换范围;代码组织和命名也便于复审。建议后续在核心递归函数中更早加入失败传播的防御性检查,降低随机路径问题的排查成本。

后续改进

如果继续迭代,可以增加可复现的随机种子参数、对批改文件给出更具体的格式错误位置,以及用 JMH 将 JVM 启动时间和核心算法耗时分开测量。这些属于扩展能力,本次实现没有为了展示技术而增加作业要求之外的运行依赖。

posted @ 2026-09-21 21:45  cxjtc  阅读(3)  评论(0)    收藏  举报