结对项目:自动生成小学四则运算题目的命令行程序

1.项目成员

2.PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 30
· Estimate · 估计这个任务需要多少时间 300 400
Development 开发 200 130
· Analysis · 需求分析(包括学习新技术) 30 60
· Design Spec · 生成设计文档 30 30
· Design Review · 设计复审(和同事审核设计文档) 10 10
· Coding Standard · 代码规范(为目前的开发制定合适的规范) 10 20
· Design · 具体设计 60 50
· Coding · 具体编码 300 400
· Code Review · 代码复审 30 60
· Test · 测试(自我测试,修改代码,提交修改) 30 40
Reporting 报告 30 30
· Test Report · 测试报告 60 60
· Size Measurement · 计算工作量 20 20
· Postmortem & Process Improvement Plan · 事后总结,并提出过程改进计划 50 50
合计 1450 1320

3.性能分析

性能热点分布 (优化后)

20% | ████████████████████░░░░░░░░░░░░░░░░░░░░░░ | main.(Expr).Eval
20% | ████████████████████░░░░░░░░░░░░░░░░░░░░░░ | main.(
Expr).ExprString
20% | ████████████████████░░░░░░░░░░░░░░░░░░░░░░ | runtime.acquirem
20% | ████████████████████░░░░░░░░░░░░░░░░░░░░░░ | runtime.getMCache
20% | ████████████████████░░░░░░░░░░░░░░░░░░░░░░ | sync.(*Mutex).Unlock

关键热点函数分析

    表达式求值与字符串化 (40% 总CPU时间)

    • main.(*Expr).Eval : 20% - 表达式求值计算
    • main.(*Expr).ExprString : 20% - 生成表达式字符串

    运行时与同步开销 (40% 总CPU时间)

    • runtime.acquirem : 20% - 获取M结构(goroutine调度)
    • runtime.getMCache : 20% - 内存分配缓存获取
    • sync.(*Mutex).Unlock : 20% - 互斥锁解锁操作

    累积调用热点 (未显示在flat列,但在cum列)

    • fmt.Sprintf : 40% - 字符串格式化(主要在 CanonicalKey 中)
    • runtime.mallocgc : 40% - 内存分配与垃圾回收
    • runtime.slicebytetostring : 40% - 字节切片转字符串

小学四则运算题目生成器 - 设计实现过程

1. 系统架构

1.1 核心组件

系统由5个主要组件构成,形成清晰的分层架构:

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│    主程序    │────▶│  题目生成器  │────▶│  表达式树   │
│   main.go   │     │ generator.go │     │   expr.go   │
└─────────────┘     └─────────────┘     └─────────────┘
       │                                       │
       │                                       │
       ▼                                       ▼
┌─────────────┐                        ┌─────────────┐
│  表达式解析器 │                        │   有理数    │
│  parser.go  │                        │ rational.go │
└─────────────┘                        └─────────────┘

1.2 类与函数关系

核心数据结构

  • Rational: 有理数类型,支持分数运算
  • Expr: 表达式树节点,可表示叶节点(数值)或内部节点(运算符)
  • Op: 运算符枚举类型

主要流程

  1. 生成模式mainGenerateProblemsgenExprExpr.Eval/Expr.ExprString
  2. 判分模式maingradeParseExpressionExpr.Eval → 比较结果

2. 关键组件详解

2.1 有理数系统 (rational.go)

┌───────────────────────────┐
│        Rational           │
├───────────────────────────┤
│ - Num: int64              │
│ - Den: int64              │
├───────────────────────────┤
│ + NewRational(num, den)   │
│ + Add/Sub/Mul/Div         │
│ + Less/LessEq/Equals      │
│ + String()                │
│ + IsZero()                │
└───────────────────────────┘
  • 实现分数的四则运算
  • 自动约分和标准化
  • 支持整数、真分数、带分数的字符串表示

2.2 表达式树 (expr.go)

┌───────────────────────────┐
│           Expr            │
├───────────────────────────┤
│ - Op: Op                  │
│ - Left, Right: *Expr      │
│ - Value: Rational         │
│ - Operators: int          │
├───────────────────────────┤
│ + NewLeaf/NewNode         │
│ + Eval()                  │
│ + ExprString()            │
│ + CanonicalKey()          │
└───────────────────────────┘
  • 表示数值叶节点或运算符节点
  • 支持表达式求值和字符串化
  • 生成规范化键用于去重

2.3 表达式解析器 (parser.go)

┌───────────────────────────┐
│         parser            │
├───────────────────────────┤
│ - src: []rune             │
│ - i: int                  │
├───────────────────────────┤
│ + parseExpr()             │
│ + parseTerm()             │
│ + parseFactor()           │
└───────────────────────────┘
  • 递归下降解析器,实现运算符优先级
  • 支持括号、四则运算符和各种数值格式
  • 使用经典的文法规则:E → T {(+|-)T}, T → F {(×|÷)F}, F → number | (E)

2.4 题目生成器 (generator.go)

┌───────────────────────────┐
│    GenerateProblems       │
└───────────────────────────┘
           │
           ▼
┌───────────────────────────┐
│        genExpr            │
└───────────────────────────┘
           │
           ▼
┌───────────────────────────┐
│      randomNumber         │
└───────────────────────────┘
  • 生成满足约束的随机表达式
  • 确保除法结果为真分数
  • 实现表达式去重
  • 生成题目和答案字符串

3. 关键流程图

3.1 题目生成流程

┌─────────┐     ┌─────────────┐     ┌───────────────┐     ┌─────────────┐
│  开始   │────▶│ 生成随机表达式│────▶│ 检查约束条件   │────▶│ 计算规范化键 │
└─────────┘     └─────────────┘     └───────────────┘     └─────────────┘
                                            │                     │
                                            │                     │
                                            ▼                     ▼
┌─────────────┐     ┌─────────────┐     ┌───────────────┐     ┌─────────────┐
│ 返回题目和答案│◀────│ 添加到结果集  │◀────│  检查是否重复  │◀────│  计算表达式值 │
└─────────────┘     └─────────────┘     └───────────────┘     └─────────────┘

3.2 表达式求值流程

┌─────────┐
│  Eval() │
└────┬────┘
     │
     ▼
┌────────────────┐     ┌─────────────┐
│ 是否为叶节点?   │────▶│ 返回Value    │
└────────┬───────┘     └─────────────┘
         │ 否
         ▼
┌────────────────┐     ┌─────────────┐
│ 递归计算左右子树 │────▶│ Left.Eval()  │
└────────┬───────┘     └──────┬──────┘
         │                    │
         │                    ▼
         │              ┌─────────────┐
         │              │ Right.Eval() │
         │              └──────┬──────┘
         │                     │
         ▼                     ▼
┌────────────────┐     ┌─────────────┐
│ 根据Op执行运算  │◀────│ 获取左右结果  │
└────────┬───────┘     └─────────────┘
         │
         ▼
┌────────────────┐
│ 返回计算结果    │
└────────────────┘

4. 实现过程

4.1 开发顺序

  1. 基础数据结构:首先实现 Rational 类,为所有计算提供基础
  2. 表达式树:实现 Expr 结构和基本操作
  3. 解析器:开发递归下降解析器,支持表达式解析
  4. 生成器:实现随机表达式生成,并确保满足约束
  5. 主程序:整合命令行参数处理、文件IO和性能分析

4.2 关键算法

  1. 表达式生成与约束

    • 递归生成表达式树,随机分配运算符数量
    • 针对减法和除法特别处理,确保结果非负且除法结果为真分数
    • 使用规范化键进行去重,考虑加法和乘法的交换性
  2. 表达式解析

    • 使用递归下降解析器实现运算符优先级
    • 支持括号、整数、真分数和带分数格式
  3. 性能优化

    • 预分配哈希表容量减少重新哈希开销
    • 热点函数优化:表达式求值、字符串化和规范化键生成

5.代码说明

1. 核心数据结构

有理数结构 (rational.go)

// Rational 表示一个有理数(分数),支持四则运算和比较
type Rational struct {
    Num int64 // 分子
    Den int64 // 分母
}

// NewRational 创建一个新的有理数并自动约分
func NewRational(num, den int64) Rational {
    if den == 0 {
        panic("denominator cannot be zero") // 分母不能为零
    }
    // 规范化符号并约分
    if den < 0 {
        num = -num
        den = -den
    }
    g := gcd(abs64(num), den) // 使用最大公约数约分
    return Rational{Num: num / g, Den: den / g}
}

// String 将有理数格式化为字符串:整数、真分数a/b或带分数k'a/b
func (r Rational) String() string {
    if r.Den == 1 {
        return fmt.Sprintf("%d", r.Num) // 整数形式
    }
    if r.Num < 0 {
        // 处理负数
        abs := NewRational(-r.Num, r.Den)
        s := abs.String()
        return "-" + s
    }
    n := r.Num
    d := r.Den
    if n < d {
        return fmt.Sprintf("%d/%d", n, d) // 真分数形式
    }
    k := n / d    // 整数部分
    rem := n % d  // 余数部分
    if rem == 0 {
        return fmt.Sprintf("%d", k)
    }
    return fmt.Sprintf("%d'%d/%d", k, rem, d) // 带分数形式
}

表达式树 (expr.go)

// Op 表示四则运算符类型
type Op int

const (
    OpNone Op = iota // 叶节点(数值)
    OpAdd            // 加法
    OpSub            // 减法
    OpMul            // 乘法
    OpDiv            // 除法
)

// Expr 表示一个表达式树节点
type Expr struct {
    Op        Op        // 运算符类型,OpNone表示叶节点
    Left      *Expr     // 左子表达式
    Right     *Expr     // 右子表达式
    Value     Rational  // 叶节点的值
    Operators int       // 子树中的运算符数量
}

// NewLeaf 创建一个数值叶节点
func NewLeaf(v Rational) *Expr { 
    return &Expr{Op: OpNone, Value: v, Operators: 0} 
}

// NewNode 创建一个运算符节点
func NewNode(op Op, l, r *Expr) *Expr {
    return &Expr{Op: op, Left: l, Right: r, Operators: l.Operators + r.Operators + 1}
}

// Eval 计算表达式的值
func (e *Expr) Eval() Rational {
    if e.Op == OpNone {
        return e.Value // 叶节点直接返回值
    }
    
    // 递归计算左右子表达式
    lv := e.Left.Eval()
    rv := e.Right.Eval()
    
    // 根据运算符执行相应运算
    switch e.Op {
    case OpAdd:
        return lv.Add(rv)
    case OpSub:
        return lv.Sub(rv)
    case OpMul:
        return lv.Mul(rv)
    case OpDiv:
        return lv.Div(rv)
    }
    return Rational{} // 不应该到达这里
}

2. 题目生成算法 (generator.go)

// GenerateProblems 生成n个满足约束条件的不重复题目,数值范围为r
func GenerateProblems(n, r int) ([]string, []string, error) {
    // 预估容量,减少哈希表扩容与重新哈希的开销
    seen := make(map[string]struct{}, n*2)
    exercises := make([]string, 0, n)
    answers := make([]string, 0, n)

    // 设置尝试上限以避免无限循环
    maxAttempts := n * 200
    attempts := 0
    for len(exercises) < n && attempts < maxAttempts {
        attempts++
        // 随机生成1-3个运算符的表达式
        maxOps := 1 + rand.Intn(3) 
        expr := genExpr(r, maxOps)
        
        // 验证表达式是否满足约束条件
        if !validateExprConstraints(expr) {
            continue
        }
        
        // 生成规范化键用于去重
        key := expr.CanonicalKey()
        if _, ok := seen[key]; ok {
            continue // 已存在,跳过
        }
        seen[key] = struct{}{}

        // 生成题目和答案字符串
        exStr := expr.ExprString(true) + " = " 
        ans := expr.Eval().String()
        exercises = append(exercises, exStr)
        answers = append(answers, ans)
    }
    
    if len(exercises) < n {
        return nil, nil, errors.New("未能在合理尝试次数内生成足够的不重复题目,请增大范围或减少数量")
    }
    return exercises, answers, nil
}

// genExpr 递归构建一个具有指定运算符数量的表达式
func genExpr(r, maxOps int) *Expr {
    if maxOps == 0 {
        // 生成叶节点(随机数值)
        v := randomNumber(r)
        return NewLeaf(v)
    }
    
    // 随机选择运算符
    op := randomOp()
    
    // 在左右子树之间分配运算符数量
    leftOps := 0
    if maxOps > 1 {
        leftOps = rand.Intn(maxOps) // 0..maxOps-1
    }
    rightOps := maxOps - 1 - leftOps
    
    // 递归生成左右子表达式
    left := genExpr(r, leftOps)
    right := genExpr(r, rightOps)

    // 根据约束条件调整表达式
    switch op {
    case OpSub:
        // 确保左值 >= 右值,避免负数结果
        lv := left.Eval()
        rv := right.Eval()
        if lv.Less(rv) {
            // 交换左右子树以满足约束
            left, right = right, left
        }
    case OpDiv:
        // 确保右值非零且左值 < 右值(结果为真分数)
        lv := left.Eval()
        rv := right.Eval()
        // 尝试重新生成右子树直到非零
        tries := 0
        for rv.IsZero() && tries < 10 {
            right = genExpr(r, rightOps)
            rv = right.Eval()
            tries++
        }
        // 尝试调整以确保结果为真分数
        if !lv.Less(rv) {
            // 如果不满足,尝试交换或重新生成
            left, right = right, left
            lv = left.Eval()
            rv = right.Eval()
            
            // 如果仍不满足,使用特殊生成函数
            if rv.IsZero() || !lv.Less(rv) {
                left = genSmallExpr(r, leftOps)  // 生成较小的左值
                right = genLargeExpr(r, rightOps, left.Eval()) // 生成较大的右值
            }
        }
    }
    return NewNode(op, left, right)
}

3. 表达式解析器 (parser.go)

// ParseExpression 将中缀表达式字符串解析为表达式树
// 支持括号、+、-、×、÷和数字:整数、分数a/b、带分数k'a/b
func ParseExpression(s string) (*Expr, error) {
    p := &parser{src: []rune(s), i: 0}
    expr, err := p.parseExpr()
    if err != nil {
        return nil, err
    }
    // 跳过尾部空格
    p.skipSpaces()
    if p.i != len(p.src) {
        return nil, fmt.Errorf("多余字符: %s", string(p.src[p.i:]))
    }
    return expr, nil
}

// 递归下降解析器实现
// 语法规则:
// E -> T {( + | - ) T}*  // 表达式由项和加减运算组成
// T -> F {( × | ÷ ) F}*  // 项由因子和乘除运算组成
// F -> number | ( E )    // 因子是数字或括号表达式
func (p *parser) parseExpr() (*Expr, error) {
    // 解析第一个项
    left, err := p.parseTerm()
    if err != nil {
        return nil, err
    }
    
    // 循环处理后续的加减运算
    for {
        p.skipSpaces()
        if p.i >= len(p.src) {
            break
        }
        op, ok := p.tryAddSub()
        if !ok {
            break
        }
        right, err := p.parseTerm()
        if err != nil {
            return nil, err
        }
        left = NewNode(op, left, right)
    }
    return left, nil
}

// parseTerm 解析一个项(乘除优先级)
func (p *parser) parseTerm() (*Expr, error) {
    // 解析第一个因子
    left, err := p.parseFactor()
    if err != nil {
        return nil, err
    }
    
    // 循环处理后续的乘除运算
    for {
        p.skipSpaces()
        if p.i >= len(p.src) {
            break
        }
        op, ok := p.tryMulDiv()
        if !ok {
            break
        }
        right, err := p.parseFactor()
        if err != nil {
            return nil, err
        }
        left = NewNode(op, left, right)
    }
    return left, nil
}

4. 主程序流程 (main.go)

func main() {
    rand.Seed(time.Now().UnixNano())

    // 生成模式的命令行参数
    n := flag.Int("n", 10, "生成题目数量(默认10)")
    r := flag.Int("r", -1, "数值范围(必填,生成模式)。数值均小于该范围")

    // 判分模式的命令行参数
    efile := flag.String("e", "", "题目文件路径(判分模式)")
    afile := flag.String("a", "", "答案文件路径(判分模式)")

    // 性能分析参数
    cpuprofile := flag.String("cpuprofile", "", "CPU性能分析输出文件(例如 cpu.prof)")
    memprofile := flag.String("memprofile", "", "内存性能分析输出文件(例如 mem.prof)")

    // 设置使用说明
    flag.Usage = func() {
        exe := filepath.Base(os.Args[0])
        fmt.Fprintf(os.Stderr, "用法:\n")
        fmt.Fprintf(os.Stderr, "  生成题目: %s -r <范围> [-n <数量>]\n", exe)
        fmt.Fprintf(os.Stderr, "  判定对错: %s -e <exercises>.txt -a <answers>.txt\n", exe)
        fmt.Fprintf(os.Stderr, "说明:\n")
        fmt.Fprintf(os.Stderr, "  -r 必须在生成模式下提供,表示自然数、真分数及分母的取值均在 [0, r) / [1, r) 内。\n")
        fmt.Fprintf(os.Stderr, "  生成的表达式满足:不产生负数;除法子表达式结果为真分数;运算符个数≤3;去重考虑 + 与 × 的交换等价。\n")
    }

    flag.Parse()

    // 启动CPU性能分析(如果请求)
    var stopCPU func()
    if *cpuprofile != "" {
        // 创建并启动CPU分析
        f, err := os.Create(*cpuprofile)
        if err != nil {
            fmt.Fprintln(os.Stderr, "无法创建CPU分析文件:", err)
        } else {
            if err := pprof.StartCPUProfile(f); err != nil {
                fmt.Fprintln(os.Stderr, "启动CPU分析失败:", err)
                f.Close()
            } else {
                stopCPU = func() {
                    pprof.StopCPUProfile()
                    f.Close()
                }
            }
        }
    }

    // 根据参数决定运行模式
    if *efile != "" || *afile != "" {
        // 判分模式
        if *efile == "" || *afile == "" {
            fmt.Fprintln(os.Stderr, "错误:判分模式需同时提供 -e 与 -a。")
            flag.Usage()
            os.Exit(1)
        }
        if err := grade(*efile, *afile); err != nil {
            fmt.Fprintln(os.Stderr, "判分失败:", err)
            os.Exit(1)
        }
        if stopCPU != nil { stopCPU() }
        return
    }

    // 生成模式
    if *r <= 0 {
        fmt.Fprintln(os.Stderr, "错误:生成模式必须提供 -r 参数且为正整数。")
        flag.Usage()
        os.Exit(1)
    }
    if *n <= 0 {
        fmt.Fprintln(os.Stderr, "错误:生成数量 -n 必须为正整数。")
        os.Exit(1)
    }

    // 生成题目和答案
    exercises, answers, err := GenerateProblems(*n, *r)
    if err != nil {
        fmt.Fprintln(os.Stderr, "生成题目失败:", err)
        os.Exit(1)
    }

    // 将题目和答案写入文件
    if err := writeLines("Exercises.txt", exercises); err != nil {
        fmt.Fprintln(os.Stderr, "写Exercises.txt失败:", err)
        os.Exit(1)
    }
    if err := writeLines("Answers.txt", answers); err != nil {
        fmt.Fprintln(os.Stderr, "写Answers.txt失败:", err)
        os.Exit(1)
    }

    fmt.Printf("已生成 %d 道题目到 Exercises.txt,并写入答案到 Answers.txt\n", len(exercises))
    if stopCPU != nil { stopCPU() }
}

7.项目小结

1.先生成10个例子,只实现加法
2.逐步完善,实现四则运算
3.随机生成四则运算
4.更改四则运算个数
5.写进文档
6.从外部导入文件路径,判断其中的个数
7.写算法,逐个判断文件中的算数表达式与答案
8.将判断结果写入文档
9.逐个测试

团队合作心得:
第一次结对项目还是完成的不是很理想,一开始两个人一起讨论思路总是有出入,总是不统一,到后来统一之后,代码方面两个人也是有很多的歧义,总不如一个人来的好,但最终还是求同存异,完成了这一个结对项目,也加强了彼此的团队团结意思。

posted @ 2025-10-18 20:47  罗凯夫  阅读(64)  评论(0)    收藏  举报