第三周项目

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/homework/15694
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/homework/15694
这个作业的目标 四则运算题目生成与批改程序

邱启涵3124005284https://github.com/QQH1115/homework3

四则运算题目生成与批改程序

PSP 表格

PSP 阶段 预估时间(分钟) 实际时间(分钟)
计划
· 需求分析 20 25
· 生成设计文档 15 20
开发
· 设计复审 10 15
· 代码规范 10 10
· 具体设计 30 40
· 具体编码 120 180
· 代码复审 20 30
· 测试 40 60
报告
· 测试报告 15 20
· 计算工作量 5 5
· 事后总结 15 20
合计 300 425

一、效能分析

改进思路

第一轮优化:文件 I/O 瓶颈

原代码 filewrite 每写一道题就打开/关闭文件两次,n=10000 时打开/关闭 20000 次。改为文件流提到循环外,只打开一次后,std::basic_filebuf 占比从 57% 降到 0%。

第二轮优化:标准库调用开销

  1. std::to_string 换成 std::to_chars(不分配内存,直接写缓冲区)
  2. std::stoll 换成 std::from_chars
  3. std::isdigit/std::isspace 改成手写字符判断
  4. std::string 用 reserve(64) 预留空间
  5. 随机分布对象提到循环外,避免重复构造

性能分析图

优化前:

file

优化后:

file1

消耗最大的函数: 优化前是 std::basic_filebuf(57%),已消除;当前剩余热点是 main(42%)和 infixToPostfix(15%)。


二、设计实现过程

代码组织

核心模块:

模块 作用
generate(ll r) 生成一道随机四则运算题目
struct Fraction 分数类,支持 + - * / 和约分
calculate(string expr) 计算表达式,返回分数字符串

辅助函数:

函数 作用
wrong() 打印用法帮助
priority(char op) 返回运算符优先级
isOperator(char c) 判断是否是运算符
infixToPostfix(expr) 中缀转后缀(调度场算法)
evalPostfix(postfix) 后缀表达式求值
filewrite(file, content) 追加写文件

函数关系

main
 ├── generate(r)              // 生成题目
 ├── calculate(problem)       // 计算答案
 │    ├── infixToPostfix      // 中缀转后缀
 │    │    ├── priority       // 优先级判断
 │    │    └── isOperator     // 运算符判断
 │    └── evalPostfix         // 后缀求值
 │         └── Fraction        // 分数运算
 └── filewrite                // 写入文件

关键函数流程

infixToPostfix(调度场算法):

遍历表达式每个字符:
  空格 → 跳过
  数字 → 读完整数字(含 a/b 分数),加入输出
  左括号 → 入栈
  右括号 → 弹出栈顶运算符直到左括号
  ÷ → 转成 %,按优先级弹出栈顶,% 入栈
  普通运算符 → 按优先级弹出栈顶,入栈
遍历结束 → 弹出栈中所有运算符

evalPostfix(后缀求值):

遍历后缀表达式每个 token:
  运算符 → 弹出两个操作数,用 Fraction 运算,结果入栈
  数字 → 转成 Fraction 入栈
最后栈中只剩一个 Fraction,就是结果

三、代码说明

关键代码 1:generate 生成题目

// 随机决定操作数个数(1~4 个)
ll num = dist14(gen), s = 0;

for (ll i = 0; i < num; i++) {
    // 随机决定是否加左括号
    if (i != num - 1) {
        ll op = dist14(gen);
        if (op == 1) { problem += "("; s++; w = 1; }
    }

    // 生成分数 a/b 并约分
    ll a = dist1r(gen), b = dist1r(gen);
    ll g = std::gcd(a, b);
    a /= g; b /= g;

    // 分母为 1 写成整数,否则写成分数
    if (b == 1) {
        auto [p, ec] = std::to_chars(buf, buf + sizeof(buf), a);
        problem.append(buf, p);
    } else {
        // ... a/b 形式
    }

    // 随机加右括号
    // 随机生成运算符 + - * ÷
}

思路: 用括号栈 s 记录未匹配的左括号数,最后一个操作数后补全所有右括号。

关键代码 2:Fraction 分数类

struct Fraction {
    ll num, den;

    // 构造函数:保证分母为正,并约分
    Fraction(ll n = 0, ll d = 1) : num(n), den(d) {
        if (den < 0) { num = -num; den = -den; }
        reduce();
    }

    void reduce() {
        ll g = std::gcd(std::abs(num), den);
        num /= g; den /= g;
    }

    Fraction operator+(const Fraction& o) const {
        return Fraction(num * o.den + o.num * den, den * o.den);
    }
    // ... - * /
};

思路: 所有运算都返回新的 Fraction,自动约分。isNegative() 用于检查负数。

关键代码 3:infixToPostfix 中 ÷ 的处理

// 除号 ÷(UTF-8 两字节)→ 转成 %
if ((unsigned char)c == 0xC3 && (unsigned char)expr[i + 1] == 0xB7) {
    while (!ops.empty() && priority(ops.top()) >= priority('%')) {
        output.push_back(std::string(1, ops.top()));
        ops.pop();
    }
    ops.push('%');
    i += 2;
    continue;
}

思路: ÷ 和分数里的 / 冲突,所以把 ÷ 转成 % 作为内部符号。priority('/') = 3 比 priority('%') = 2 高,保证 7÷2/9 中的 2/9 被整体读入,不会被拆成 7÷2÷9。


四、测试运行

测试用例(10 个)

# 输入 预期输出 说明
1 3/4+2/3 17/12 分数加法
2 1/2*4/5 2/5 分数乘法
3 5-2/7 33/7 整数减分数
4 (1/3+2)*3/5 7/5 括号优先级
5 7÷2/9 63/2 ÷ 后跟分数
6 1+2*3 7 乘法优先级
7 (1+2)*3 9 括号优先级
8 6÷3 2 整数除法
9 1/2+1/3+1/6 1 多分数连加
10 2*3/4 3/2 乘法与分数

为什么能确定程序正确

  1. 分数全程精确:用 ll 分子分母,不做浮点运算,1/3+1/6 精确得 1/2,不会有浮点误差。
  2. 自动约分:每次运算后 Fraction 构造函数自动约分,结果总是最简分数。
  3. 边界测试:
    • -n 1 -r 1:最小值测试,不会死循环
    • -n 10000 -r 100:大规模测试,能跑完
    • -n 1000 -r 5:小范围去重测试,触发保护机制
  4. 负数保护:evalPostfix 每步检查 isNegative(),保证计算过程不出负数。
  5. 括号匹配:infixToPostfix 里检查括号是否匹配,不匹配抛异常。

五、总结

遇到的困难

  1. ÷ 和 / 冲突:分数用 /,除号也用 / 时无法区分。解决方案:÷ 内部转成 %,/ 优先级更高。
  2. Windows 编码问题:÷ 是 UTF-8 字符,VS 默认 GBK 会乱码,需要另存为 UTF-8 带签名,或加 #pragma execution_character_set("utf-8")。
  3. isdigit 断言:std::isdigit 参数必须是 unsigned char,传 char 时 UTF-8 字符会触发 Debug 断言。解决方案:手写字符判断。
  4. 性能瓶颈:std::to_string 和频繁文件开关是主要瓶颈。解决方案:to_chars、文件流提到循环外。

收获

  • 掌握了调度场算法和分数运算的精确实现
  • 学会了用 VS 性能分析工具定位瓶颈
  • 理解了 to_chars/from_chars 比 to_string/stoll 快的原因
  • 认识到文件 I/O 是常见的性能陷阱
posted @ 2026-09-22 20:00  QQH1115  阅读(8)  评论(0)    收藏  举报