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
: 运算符枚举类型
主要流程:
- 生成模式:
main
→GenerateProblems
→genExpr
→Expr.Eval
/Expr.ExprString
- 判分模式:
main
→grade
→ParseExpression
→Expr.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 开发顺序
- 基础数据结构:首先实现
Rational
类,为所有计算提供基础 - 表达式树:实现
Expr
结构和基本操作 - 解析器:开发递归下降解析器,支持表达式解析
- 生成器:实现随机表达式生成,并确保满足约束
- 主程序:整合命令行参数处理、文件IO和性能分析
4.2 关键算法
-
表达式生成与约束:
- 递归生成表达式树,随机分配运算符数量
- 针对减法和除法特别处理,确保结果非负且除法结果为真分数
- 使用规范化键进行去重,考虑加法和乘法的交换性
-
表达式解析:
- 使用递归下降解析器实现运算符优先级
- 支持括号、整数、真分数和带分数格式
-
性能优化:
- 预分配哈希表容量减少重新哈希开销
- 热点函数优化:表达式求值、字符串化和规范化键生成
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.逐个测试
团队合作心得:
第一次结对项目还是完成的不是很理想,一开始两个人一起讨论思路总是有出入,总是不统一,到后来统一之后,代码方面两个人也是有很多的歧义,总不如一个人来的好,但最终还是求同存异,完成了这一个结对项目,也加强了彼此的团队团结意思。