OO U1 博客小结

王嘉琪-ZC061017 2026-04-01 18:18:09

OO第一单元博客作业

一、基于度量分析自己的程序结构

1.1 第一次作业

第一次作业的任务是单变量多项式的括号展开与合并同类项。我的整体结构核心是“递归下降解析 + 多项式统一求值”。

核心类度量表

类名属性个数方法个数代码规模说明
Main0229 行输入输出控制
Parser28143 行递归下降解析核心
Expr2330 行表达式层,管理加减
Term1322 行项层,管理乘法
Factor013 行因子抽象类
NumberFactor1214 行常数因子
VarFactor2216 行变量因子
ExprFactor3220 行括号表达式因子
Poly111139 行多项式运算与输出核心

方法分析

第一次作业中,复杂度最高的方法主要有两个。第一个是 Parser.parseFactor(),负责处理前导符号、括号因子、变量因子、常数因子和指数读取,分支最多,也是最容易出错的地方。第二个是 Poly.toExprString(),负责最终输出格式,既要保证语义正确,又要保证没有多余括号和空格。

从控制分支和圈复杂度的角度看,ParserPoly 是整个程序中最重的两个类;而 ExprTermFactor 体系的职责都比较单一,复杂度相对低。

类图

img

类设计说明

Main 的职责只有读入、去空白、调用解析、输出结果,职责很单一。
Parser 负责按照文法把字符串解析成对象结构,是第一次作业的控制核心。
Expr 表示加减结构,Term 表示乘法结构,Factor 统一不同因子的求值接口。
NumberFactorVarFactorExprFactor 对应三类因子,这种继承结构使 Term 不需要写大量分支判断。
Poly 用“指数到系数的映射”表示单变量多项式,便于合并同类项,也便于实现加减乘幂。

结构优缺点

优点:语法层和计算层分离得比较清楚,Expr-Term-FactorPoly 分工明确,代码可读性比较好。
缺点:第一次作业的 Poly 只适合单变量多项式,一旦后续出现更一般的表达式结构,这个抽象就会受到限制。另外,Expr 采用“符号列表 + 项列表”的并行数组设计,写起来方便,但内聚性一般。


1.2 第二次作业

第二次作业在第一次作业的基础上加入了 exp、自定义函数、选择式因子,程序开始从“纯多项式展开器”向“更一般的表达式处理系统”过渡。

核心类度量表

类名属性个数方法个数代码规模说明
Main0249 行输入输出控制
FunctionDef1211 行普通函数定义封装
Parser313244 行解析核心
Expr2330 行表达式层
Term1322 行项层
Factor 体系多个多个约 100+ 行新增 exp、函数调用、选择式等子类
Poly115+421 行广义多项式核心
MonoKey210+约 90 行单项式键抽象

方法分析

第二次作业最复杂的方法仍然集中在 ParserPoly 中。
Parser.parseFactor() 比第一次作业更复杂,负责 exp(...)f(...) 和选择式因子的分派。
Parser.parseChooseFactor() 也比较复杂,不仅要处理 [(A==B)?C:D] 这种结构,还要正确识别条件部分和两个分支。
Poly 中的 normalize()toExprString()toFactorString() 和一些和 exp 相关的输出优化方法也变得更重,既要维护语义,又要尽量缩短输出。

类图

img

类设计说明

第二次作业中,Factor 体系成为扩展中心。
在第一次作业的 NumberFactorVarFactorExprFactor 之外,我又增加了 ExpFactorChooseFactorFuncCallFactor 等新因子类型。这样做的好处是新增语法大多只需要增加新的因子子类,而不必推翻整个文法结构。
FunctionDef 把普通函数定义独立出来,使函数调用不只是字符串替换,而是一个明确的对象行为。
Poly 则从第一次作业中的“单变量多项式”推广成了“支持 xexp(...) 的广义单项式系统”。它内部不再只是记录变量指数,而是通过 MonoKey 同时记录变量幂次和指数函数参数。

结构优缺点

优点:延续了第一次作业中正确的文法分层,整体结构没有被推翻,扩展点也比较明确。
缺点: Poly 的职责进一步膨胀,它不仅负责运算,还承担了规范化和输出优化的任务。
同时,Parser 中的分支也明显增多,尤其是 parseFactor() 逐渐变成高风险方法。


1.3 第三次作业

第三次作业加入双变量、求导算子和递推函数,程序从“表达式展开与替换系统”进一步发展成了一个简化版符号计算系统。

核心类度量表

类名属性个数方法个数代码规模说明
Main0390 行总体流程控制
Environment2420 行保存函数环境
EvalContext3324 行保存求值上下文
ParseConfig6651 行解析上下文限制
FunctionDef1211 行普通函数定义
RecFunctionDef4+464 行递推函数与缓存
Parser316273 行解析核心
Expr2330 行表达式层
Term1322 行项层
Factor 体系多个多个约 190 行新增导数、递推调用等因子
Poly123 左右446 行双变量广义多项式核心

方法分析

第三次作业中,复杂度最高的类仍然是 ParserPoly
Parser.parseFactor()Parser.parseFunctionLikeFactor() 负责处理普通函数、递推函数、占位调用和求导因子,分支最密。
Poly.derivative()Poly.normalize()Poly.toExprString() 则是语义最重的几个方法,既要保证求导结果正确,又要保证输出满足必要括号和性能要求。
另外,RecFunctionDef.apply() 虽然代码不算最长,但它位于递推求值的热点路径上,对整体性能影响很大。

类图

img

类设计说明

第三次作业中最有价值的三个新抽象是 EnvironmentEvalContextParseConfig
Environment 负责保存普通函数和递推函数的定义。
EvalContext 负责保存当前实参、全局环境和递推层数,使求值过程具有统一的动态上下文。
ParseConfig 负责描述当前解析环境允许出现哪些语法成分,例如函数定义体中不允许 y、不允许求导、不允许选择式等,这样可以把题目限制显式写进程序结构里。
RecFunctionDef 负责递推函数展开,并且加入了缓存,避免同一 (序号, 实参) 被重复计算。
Poly 则进一步扩展为支持 x^a * y^b * exp(P(x,y)) 这种二元广义单项式表示,并统一承接加减乘幂、求导、规范化和输出。

结构优缺点

优点:整体骨架稳定,扩展路线清楚,尤其是上下文抽象比前两次更成熟。
缺点: Poly 过重,它同时承担了数据表示、运算、求导、输出和局部优化,职责偏多。
另外,Factor.java 中放了较多内部逻辑子类,实现上方便,但模块粒度仍然偏粗。


1.4 总结

三次作业中,真正稳定下来的部分是 Expr-Term-Factor 这一文法骨架;变化最大的部分则是 Poly 的具体表示方式。
第一次作业的 Poly 只支持单变量多项式;第二次作业推广为支持 exp(...) 的广义单项式;第三次作业又推广为支持双变量与求导的广义单项式系统。
这说明我的架构并不是每次都推翻重写,而是在连续迭代中逐渐识别出了哪些部分是稳定骨架,哪些部分必须随需求继续推广。


二、架构设计体验

2.1 三次作业中的架构如何逐步成型

第一次作业中,我的主要目标是把字符串表达式稳定地转成对象结构,再统一求值。因此我采用了递归下降解析,并建立了 Expr-Term-Factor 的层次,同时用 Poly 表示单变量多项式。这个阶段的重点是“先把表达式解析对,再把展开和合并同类项做对”。

第二次作业加入 exp、自定义函数和选择式后,第一次作业中“所有表达式最终都能压成普通多项式”的前提开始失效。面对这个变化,我没有推翻原有结构,而是保留文法骨架,把扩展集中在两个位置:一是继续增加新的因子子类,二是推广 Poly 的单项表示方式,使其能够表达 xexp(...) 共同组成的广义单项式。这个阶段的重点变成了“支持更一般的表达式结构”。

第三次作业又加入双变量、求导算子和递推函数,程序开始真正具备符号运算特征。到了这一步,除了继续扩展因子体系和 Poly 之外,我还显式引入了 EnvironmentEvalContextParseConfig。这说明程序已经不仅仅是在做静态展开,而是开始拥有“环境”“上下文”“受限文法”这些更成熟的运行时抽象。

2.2 重构前后最明显的变化

如果从结构上看,三次作业最大的变化不是 ExprTerm,因为这两个类基本一直比较稳定;最明显的变化集中在以下三点:

  1. Factor 体系不断扩展,逐渐成为新增语法的主要落点;
  2. Poly 的表示能力不断被推广,从普通多项式变成广义双变量表达式;
  3. 从第三次作业开始,程序出现了显式上下文对象,架构成熟度明显高于前两次。

这也说明,我的重构不是“整体推翻式重写”,而更像是“保留稳定骨架,逐渐推广核心抽象”。

2.3 自定义一个新的迭代场景

如果下一次迭代新增的是二阶偏导,例如 dxx(E)dxy(E)dyy(E),我认为当前最终设计仍然有一定可扩展性。

原因是:

  1. 第三次作业已经支持双变量和一阶偏导,Poly 已经能够区分 xy
  2. 现有的 DerivativeFactor 已经把“求导算子”作为独立因子处理,继续增加新的导数算子时,不需要推翻原有文法;
  3. Poly.derivative() 已经存在,只需要在现有基础上做多次求导组合即可;
  4. EnvironmentEvalContext 不需要大改,因为二阶导不会改变函数环境和代换方式。

当然,这种扩展也会进一步增加 Poly 的压力。如果后续需求继续增加,例如更多运算符或者更多函数类型,那么仅靠继续扩展 Poly 可能就不够了,需要把“表示”“运算”“输出”进一步拆开。


三、分析自己程序的 bug

3.1 第二次作业:幂指数溢出导致输出错误

第二次作业中,我在强测里遇到的 bug:输入中每个显式指数都不超过 8,但表达式通过多层幂嵌套后,会在语义上产生极大的总指数。例如类似 ((((x^8)^8)^8)... )^8 这样的结构,虽然输入合法,但中间结果中的指数会非常大。我的程序最终错误输出为 1

这个 bug 的根源不是单纯“某个边界条件忘了判”,而是程序内部对指数的表示不统一。解析器和多个因子类里,指数仍然主要使用 int;但在 Poly 的单项表示里,变量指数又已经使用了更大范围的数据表示。也就是说,程序只在结果层承认指数可能很大,但在前面的解析层和因子层仍然默认指数是小整数。一旦出现深层嵌套幂,这种不一致就会暴露出来。

3.2 第三次作业:CPU 时间超限

第三次作业中,我被互测攻击的点主要不是正确性错误,而是 CPU 时间超限。被攻击数据的共同特点很明显:普通函数 f(x) 的定义本身就很复杂,包含多层嵌套 exp 和幂;而待展开表达式又使用了 f(f(f(...))) 这样的多层自复合调用。

结合代码分析,这类性能 bug 的主要原因是我对普通函数调用采取了“立即代入、立即完整求值”的策略。在 FunctionDef.apply() 中,每次函数调用都会重新对整个函数体执行一次 eval;而 VarFactor.eval() 在函数体内部又会把形参 x 直接替换成实参多项式,再继续参与 pow()mul()normalize()。于是,随着函数体越来越大、调用层数越来越深,整个中间结果规模会迅速膨胀,最终表现为 CPU 时间超限。

3.3 有 bug 的方法与无 bug 的方法对比

如果把这几个 bug 放回到方法层面看,会发现一个现象:真正出问题的方法不一定是最复杂的那个。

类型方法特征
有 bug 的方法幂相关求值链、FunctionDef.apply()VarFactor.eval()代码不一定最长,但位于关键数据边界或高频热点上
相对稳定的方法Expr.eval()Term.eval()、简单常数因子求值逻辑单一,输入输出关系清楚,副作用较少

所以我认为,降低 bug 风险的方法不只是“把大方法拆小”,还包括两点:
第一,减少关键数据在不同层次上的表示不一致;
第二,识别真正的运行热点,避免在热点路径上做重复计算。


四、分析自己发现别人程序 bug 所采用的策略

第一次作业互测时,我发现一类有效的 hack 点:构造最终结果为 0 的表达式。

例如:

输入正确结果暴露的问题
x^0-10幂化简后归零,未正确输出
x-(+x)0一元正号与项抵消处理不当
++x+(-+x)-(x)+(--x)0多层符号折叠后整体归零处理不当

这些样例都合法,而且覆盖了不同来源的零结果:有的是幂化简后归零,有的是括号和一元正号参与抵消,有的是连续符号折叠后归零。很多程序在普通情况下能够输出正确结果,但一旦所有项都被消掉,就会输出空串而不是 0,最后被判成 Format Error | Empty output


五、分析自己的优化

第三次作业中,我做的主要优化是:当表达式中出现多个相同的 exp(...) 结构时,尝试把这个 exp(...) 当作公因式提出来;然后把“提取公因式后的表达式”和“不提取公因式的原式”分别转成字符串,比较两者长度,输出更短的那个。

题目的性能分和输出长度相关,在第二次作业强测汲取的经验在第三次作业中运用 。如果直接保持原样输出,表达式虽然正确,但长度可能较长;如果提取公共的 exp(...) 因子,有时能明显缩短结果。

不过,这种优化不是无条件进行的。因为提取公因式后,有时会增加括号、乘号等额外符号,导致结果反而更长。所以我没有固定采用某一种形式,而是同时保留“提取后”和“提取前”两个候选结果,最后用长度比较来选择更优输出。

这个优化的优点是实现成本不算高,而且能直接对性能判定起作用。缺点是它仍然只是局部优化,不能保证得到全局最短输出。后续如果继续改进,可以把“同时构造多个等价候选式并比较长度”的思路推广到更多等价变形上,而不只是局限于 exp(...) 公因式提取。


六、大模型相关使用

6.1 使用比例

在本单元三次作业中,我使用大模型的比例大约在 30% 左右。主体结构和主要代码仍然是自己完成的,但在优化、查错和分析边界问题时,大模型提供了比较多的辅助。

6.2 主要使用场景

我使用大模型最主要的场景有两个:

  1. 性能优化辅助:分析哪些等价变形可能缩短输出,或者某种输出形式为什么更短;
  2. bug 修复辅助:在我给出出错样例和代码后,帮助分析问题更可能出在哪一层、哪个类、哪类方法。

6.3 使用效果评价

总体来看,大模型在性能优化和 bug 修复上的完成效果是比较好的。只要我能提供足够具体的输入样例、错误现象和相关代码,它通常能较快给出比较有价值的分析方向。但在互测阶段,大模型在“主动帮我找最有杀伤力的 hack 点”这件事上并不算特别聪明。它能给出一些常见边界点,但不一定总能抓到最容易卡掉别人程序的输入。

6.4 对互测房间中 AI 使用情况的看法

本次互测房间里,我暂无明确怀疑对象,所以这一部分我不做主观判断。仅从我自己接触到的代码来看,还没有足够强的依据去认定某位同学大量使用了 AI 生成代码。


七、心得体会

第一单元给我的最大感受是:刚开始看上去像在写一个“字符串处理程序”,但做着做着就变成了一个“表达式系统设计问题”。

第一次作业时,我主要关注的是怎么把文法解析对、怎么把括号展开对;第二次作业开始,我逐渐意识到仅靠“把结果算出来”是不够的,程序内部还需要有能持续扩展的表示方式;到了第三次作业,双变量、求导和递推函数一起出现,我才真正体会到“架构设计”在连续迭代里的意义。

这一单元里,我收获最大的不是某一个具体技巧,而是两点。
第一,稳定的骨架要尽量早建立,比如 Expr-Term-Factor 这一层次在三次作业里一直都能用;
第二,真正容易出问题的地方,往往不是最显眼的地方。像指数的内部表示不一致、普通函数调用的热点性能问题,都是我在实践里吃过亏之后才真正意识到的。


八、未来方向

如果让我对第一单元课程提一点改进建议,我觉得可以从下面几个方向优化:

8.1 更早强调“数据表示”而不只是“语法解析”

很多同学第一次作业能把文法写出来,但对“表达式内部应该怎么表示”理解不够,结果第二次、第三次作业一加新需求,前面的设计就很难改。
如果课程在第一次作业前就更明确地强调“数据表示决定后续可扩展性”,大家后面重构的压力可能会小一些。

8.2 给出更多性能型样例

前期大家更容易关注正确性,但到了后面才发现,合法输入也可能因为中间结果膨胀把程序卡死。如果能更早给一些典型性能型数据,让大家意识到“热路径”和“中间结果规模”的问题,会更有帮助。

...全文
140 回复 打赏 收藏 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

309

社区成员

发帖
与我相关
我的任务
社区描述
2026年北航面向对象设计与构造
java 高校
社区管理员
  • 孙琦航
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧