当前位置: 首页 > news >正文

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

1.项目成员

  • 3123004276 罗凯夫
  • github: https://github.com/lkf233/calculate

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 = -numden = -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.Numd := r.Denif 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 intconst (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 * 200attempts := 0for 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 := 0if 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 := 0for rv.IsZero() && tries < 10 {right = genExpr(r, rightOps)rv = right.Eval()tries++}// 尝试调整以确保结果为真分数if !lv.Less(rv) {// 如果不满足,尝试交换或重新生成left, right = right, leftlv = 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.逐个测试

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

http://www.hskmm.com/?act=detail&tid=33864

相关文章:

  • 做题技巧与结论证明
  • 1. 两数之和
  • CSP-S模拟34/2025多校冲刺CSP模拟赛6
  • PostgreSQL 逻辑结构
  • 随机数技术
  • Java学习通互评5
  • 卡车厂实习第三天
  • 第六周作业---定时器
  • 『普及』浅谈图的基础
  • 被C语言链表折磨的一天 Σ( △ |||)︴
  • 运筹学在供应链优化中的实际应用
  • P6715 [CCO 2018] Fun Palace 题解
  • WebGL学习及项目实战(第03期:绘制多个点,线,面)
  • CF 359D. Pair of Numbers
  • 2025多校CSP模拟赛6
  • Java基础——类型转换,变量、常亮、作用域,基本运算符
  • 洛谷 LGR-246 S 模拟赛
  • godot3D节点本身的偏转数值错误竟会导致空间移动穿模??!
  • 题解:qoj7938 Graph Race
  • java中的初等函数
  • 分治算法
  • 专用硬件神经网络优化技术解析
  • 学习逆向的背景知识(自用)
  • Linux-网络安全私房菜(二)
  • pycharm使用远程的ssh的解释器
  • Android SSL Pinning检测利器:SSLPinDetect技术解析
  • AI元人文:社区调解的数字剧场
  • 2025年粉末冶金制品/零件厂家推荐排行榜,专业制造与高品质服务的首选!
  • Dubbo入门-Dubbo的快速使用
  • 站位2