结对项目:自动生成小学四则运算题目的命令行程序
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 = -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.逐个测试
团队合作心得:
第一次结对项目还是完成的不是很理想,一开始两个人一起讨论思路总是有出入,总是不统一,到后来统一之后,代码方面两个人也是有很多的歧义,总不如一个人来的好,但最终还是求同存异,完成了这一个结对项目,也加强了彼此的团队团结意思。