软件工程第三次作业

个人项目:小学四则运算题目自动生成程序

这个作业属于哪个课程 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.txtAnswers.txt
  • 判分模式:Myapp -e Exercises.txt -a Answers.txt,逐题判定对错,统计写入 Grade.txt

支持自然数与真分数(含 2'3/8 形式的带分数)、括号、每题 1~3 个运算符;生成过程不产生负数中间结果,除法结果必为真分数,一次运行内题目不重复(按「有限次交换 + 和 × 左右操作数」判等)。
8e35c4ad-790d-4f3f-8a08-f38dc49dd027

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 改进思路(示例叙述,请按你的真实过程改写)

  1. 查重从 O(n²) 到 O(n):最初用 List.contains 逐题比对 canonical 串,生成 1 万题时比对次数是平方级;改为 HashSet<String> 存规范串后,查重均摊 O(1),这是耗时占比最大的改动。
  2. 约束前置,减少废品:减法不够减时直接交换左右子树、除法倒置时交换,实在无法构成真分数才退化为加法,而不是生成整棵树后再丢弃重生成,显著降低了重试次数。
  3. 分数精确运算:全程 Fraction(约分后的 long 分子/分母),无浮点误差,判分环节不需要近似比较。

3.3 性能分析图

perf_analysis

4. 设计实现过程

4.1 代码组织

共 5 个类,职责单一、单向依赖:

职责
Main 命令行入口:解析参数,分发到生成模式 / 判分模式
Generator 随机生成表达式树,保证约束,用 HashSet 查重
Expression 表达式二叉树:求值 value()、查重规范串 canonical()、输出 toDisplay()
Fraction 有理数精确运算与题目格式(2'3/8)的解析/输出
Grader 判分:递归下降解析表达式并逐题比对,写 Grade.txt

依赖关系:Main → Generator → Expression → FractionMain → Grader → FractionExpression 不依赖 Generator,生成逻辑与数据结构分离。

4.2 关键设计决策

(1)查重等价类的判定——canonical 规范串

题目要求「任何两道题目不能通过有限次交换 + 和 × 左右的算术表达式变换为同一道题目」。做法:自底向上把每棵子树映射为一个规范字符串,遇到 +× 就把左右子树的规范串按字典序排列,这样同一等价类的树必然得到同一字符串,放入 HashSet 即可查重。用题目原文的例子验证:

  • (1+2)+33+(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 关键函数流程图

flowchart

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/52'3/850),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/242'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²),说明性能测试不能放到最后。
posted @ 2026-09-19 12:20  dl12  阅读(5)  评论(0)    收藏  举报