2026OOP Unit 1课程总结

朱晓天-24373340 2026-03-26 21:47:55

1

基于度量的程序结构分析

方法级别复杂度度量 (Method Complexity)

方法名 (Method)基本复杂度 (ev(G))设计复杂度 (iv(G))圈复杂度 (v(G))代码行数 (LOC)
expr.DeriveFactor.DeriveFactor(Expr, int)0111
expr.DeriveFactor.toPoly()3333
expr.ExpFactor.ExpFactor(Factor, int)0111
expr.ExpFactor.toPoly()0111
expr.Expr.addTerm(Term)0111
expr.Expr.Expr()0111
expr.Expr.toPoly()1122
expr.ExprFactor.ExprFactor(int, Expr)0111
expr.ExprFactor.toPoly()0111
expr.Num.Num(BigInteger)0111
expr.Num.toPoly()0111
expr.Poly.add(Poly)2133
expr.Poly.addTerm(TermKey, BigInteger)3223
expr.Poly.buildExp(Poly)34101020
expr.Poly.buildGcdExp(Poly)269610
expr.Poly.buildResult(int, int[], String[])3212
expr.Poly.deriveX()5144
expr.Poly.deriveY()5144
expr.Poly.equals(Object)3324
expr.Poly.getMap()0111
expr.Poly.greedyCompress(Poly)243911
expr.Poly.hashCode()0111
expr.Poly.mul(Poly)3133
expr.Poly.Poly()0111
expr.Poly.pow(int)6255
expr.Poly.simplify(StringBuilder, TermKey, BigInteger, boolean...)1841315
expr.Poly.statusCompressDP(Poly)164811
expr.Poly.substitute(Poly)3133
expr.Poly.toString()9758
expr.SelectFactor.SelectFactor(...)0111
expr.SelectFactor.toPoly()2222
expr.Term.addFactor(Factor)0111
expr.Term.Term(int)0111
expr.Term.toPoly()3323
expr.TermKey.equals(Object)4346
expr.TermKey.getExpPoly()0111
expr.TermKey.getXvarExp()0111
expr.TermKey.getYvarExp()0111
expr.TermKey.hashCode()0111
expr.TermKey.mul(TermKey)0111
expr.TermKey.TermKey(BigInteger, BigInteger, Poly)0111
expr.Var.toPoly()2112
expr.Var.Var(BigInteger, String)0111
FuncFactor.FuncFactor(Expr, String)0111
FuncFactor.toPoly()0111
Lexer.getNum()8156
Lexer.getToken()0111
Lexer.handleSign()6146
Lexer.Lexer(String)0111
Lexer.next()152913
MainClass.getFuncPolys()0111
MainClass.handleFunction(String, String, String)11636
MainClass.main(String[])1841011
Parser.parseDeriveFactor(int)2133
Parser.parseExpFactor()5155
Parser.parseExpr()2133
Parser.parseExprFactor()5244
Parser.parseFactor()3281820
Parser.Parser(Lexer)0111
Parser.parseSelectFactor()6177
Parser.parseTerm()4145
总计 (Total)289122196242
平均值 (Average)4.661.973.163.90

通过方法级别的复杂度分析,我们可以发现主要的复杂度集中在 Poly 类的 buildExpsimplify 方法中。这两个类的复杂度来源分别是复杂的字符串构建过程以及exp因子判断逻辑。这类复杂度是较难避免的,因为它们涉及到复杂的逻辑判断和字符串操作。除此之外,对于Lexer类中的next方法,其复杂度也较高,因为该方法需要处理多种情况,包括数字、符号、函数等。最应当注意的是MainClass的复杂度,其复杂度来源主要是handleFunction方法,以及递归函数的预求解,出于架构考虑,其实应当抽象出一个类专门处理递归函数。

代码规模度量 (Statistic)

源文件 (Source File)总行数 (Total Lines)源代码行数 (Source Code Lines)源代码占比 (%)注释行数 (Comment Lines)注释占比 (%)空白行数 (Blank Lines)空白占比 (%)
DeriveFactor.java221986%00%314%
ExpFactor.java231983%00%417%
Expr.java211676%00%524%
ExprFactor.java171482%00%318%
Factor.java5480%00%120%
FuncFactor.java191684%00%316%
Lexer.java746689%00%811%
MainClass.java989395%00%55%
Num.java151173%00%427%
Parser.java14413392%00%118%
Poly.java36133793%00%247%
SelectFactor.java221986%00%314%
Term.java282382%00%518%
TermKey.java493980%00%1020%
Var.java262285%00%415%
总计 (Total)92483190%00%9310%

从代码规模量的度量结果可以看出,我的架构呈现出明显的中心化趋势.PolyParser代码规模远超其他类。这说明在迭代中,我为了快速实现功能,将多项式的加减乘、状压 DP、贪心压缩等逻辑全部揉进Poly类,使其出现了上帝类的倾向,内聚度有待提高。应当考虑将优化算法单独抽离为Optimizer

面向对象经典度量

类名 (Class)类间耦合度 (CBO)继承树深度 (DIT)方法缺乏内聚度 (LCOM)子类数量 (NOC)类的响应数量 (RFC)加权方法复杂度 (WMC)
expr.DeriveFactor411064
expr.ExpFactor5110102
expr.Expr811094
expr.ExprFactor411042
expr.Factor1
expr.Num511042
expr.Poly121106498
expr.SelectFactor311043
expr.Term5110125
expr.TermKey6110149
expr.Var411063
FuncFactor511062
Lexer21101423
MainClass51102618
Parser121102542
总计 (Total)217
平均值 (Average)5.711.001.000.0013.6715.50

PolyParser的耦合度都达到了全局最高的12,且Poly的类相应数量高达64。这表明在架构演进中,多项式运算引擎和语法解析器承载了过多的外部依赖和调用链路。虽然我的 LCOM 指标保持在极佳的 1,说明类的内部属性维护是高度一致的,但过高的外部耦合提醒我,在后续开发中应当引入类似工厂模式或接口分离,以降低核心类的通讯成本。

架构图

img

我的架构主要分为三层。左侧是基于LexerParser的解析链路,中间是自顶向下的AST树状结构,并通过在AST的每层节点上都实现toPoly方法以达到从字符串到抽象数据的转换,这样做的好处是,后续的所有计算可以通过poly类内部的计算完成,耦合性较低,架构缺陷是过于依赖Poly类,该类的复杂度较高。TermKey是Poly类中的特征信息,通过该类结合HashMap可以实现O(1)级别的查找操作。

设计考虑

MainClass:主类,实现IO、空白字符替换和递归函数预求值,只完成最基本的操作与替换
Lexer:词法分析器,将字符串拆分为一个个Token,并处理连续符号
Parser:语法分析器,将Token序列利用递归下降法解析为AST
Expr:表达式,维护所有项,规定所有表达式实现toPoly方法
Term:项,维护每一项的符号和因子,实现toPoly方法,之所以将符号全部交给Term,是因为这样做可以只实现add方法,减轻Poly工作量
Factor:因子接口,规定所有因子实现toPoly方法
Poly:多项式运算引擎,实现多项式的加减乘除、求导、求值等操作
TermKey:多项式特征信息,用于在HashMap中查找多项式
Num:常数因子。AST 的最底层叶子节点,内部封装一个BigInteger存放精确数值,toPoly时直接返回一个常数多项式。
Var:变量因子。记录自变量名称及其指数,在toPoly阶段负责将其转化为底层运算引擎可识别的标准幂函数多项式。
ExprFactor:表达式因子。其内部包裹着一个Expr类对象。在toPoly时直接调用Expr的转换方法,解决了任意深度的括号嵌套问题。
ExpFactor:指数因子。内部维护底部的表达式或因子,toPoly阶段负责构建带自然底数exp的数学模型,同时也是触发中、小规模数据“贪心与状压DP乘开优化”的入口。
DeriveFactor:求导算子因子。保存求导目标变量(如dx,dy)及内部表达式,在AST转换为多项式的阶段,直接调用底层Poly的deriveX()或deriveY(),将求导操作转化为代数运算。
FuncFactor:自定义函数调用因子。保存函数标识与实参列表,在解析或转换阶段负责完成实参向形参的映射与代入,配合主类的预处理实现函数的等价替换。
SelectFactor:选择式因子。用于处理指导书中规定的条件求值与选择逻辑,通过解析内部的分支条件,在toPoly计算时动态返回正确的代数结果多项式。

架构设计体验

在三次作业的迭代过程中,我的架构并没有经历性重构。这极大程度上得益于我在第一次作业就确立的AST解析前端与底层代数引擎完全分离的核心思想。

在第一次作业中,我建立了标准的递归下降解析链路,通过LexerParser将输入转化为由ExprTermFactor构成的抽象语法树(AST)。为了彻底剥离解析与计算的耦合,我设计了Poly类和TermKey类作为多项式的唯一代数运算载体,所有语法树节点最终都会通过调用自身的toPoly方法转化为底层数学模型。

第二次作业引入了括号嵌套,在不改变任何原有架构的基础上,仅新增了ExprFactor节点。它在实现Factor接口的同时内部包裹一个Expr对象,调用toPoly时直接透传给内部表达式,用少量代码量实现了括号解析。

第三次作业加入了求导算子和自定义函数。由于底层Poly引擎的代数封装较为合理,应对求导需求时,我只需在Poly内部扩展deriveXderiveY方法,并在AST层面增加DeriveFactor节点即可,增加变量命则不得不修改Var类,使其维护一个变量名类,并对应修改TermKey。

新迭代情景与可扩展性分析:引入三角函数

全新的迭代需求:引入三角函数因子sin(Factor)cos(Factor),内部允许嵌套任意因子,并要求支持基本的合并与化简。
只需要新建SinFactorCosFactor两个类并实现Factor接口。在词法分析阶段,让Lexer增加对sincos字符串的识别;在语法分析阶段,仅需在ParserparseFactor方法中增加两个对应分支即可。
至于数学引擎的数据结构扩充,目前的TermKey类负责维护项的特征(如维护了xy的指数)。为了支持三角函数嵌套,需在TermKey内部新增两个容器,例如HashMap<Poly, BigInteger> sinMapHashMap<Poly, BigInteger> cosMap。这里的键设为Poly正好完美契合了三角函数内部可能嵌套任意复杂多项式表达式的需求,而值则代表该三角函数整体的指数。
在扩充了TermKey的数据结构后,需要重写TermKeyequalshashCode以及mul方法,使其在合并同类项和相乘时,额外比对并合并sinMapcosMap中的内容即可。最外层的Poly类不需要大幅度改动。

bug分析、测试策略与性能优化

在本次作业中,程序的架构设计、性能优化与Bug的产生有紧密的联系。以下结合未通过的公测用例与互测情况,将程序的缺陷、优化过程及测试策略进行综合分析。

架构缺陷与 TLE 分析

在第二次作业迭代中,为了加快开发进度,我起初并未将自定义函数抽象为独立的因子类,而是直接在 MainClass 中利用正则表达式进行全局的字符串预替换。这一设计在遇到 SelectFactor(选择式因子)时TLE了。选择式逻辑本应具备短路求值的特性,但全局字符串预替换强制展开了所有可能的分支,导致在多层嵌套和复杂条件判断下,计算量呈指数级爆炸。为解决此问题,我重构并抽象出了 FuncFactor 节点,将其推迟到 AST 转换为多项式时再按需进行实参代入,恢复了短路特性。

此外,在处理深层嵌套表达式时,我还遇到了两处因缺失记忆化(缓存)导致的 TLE:

  1. toString 缓存缺失:在多层嵌套的 AST 结构中,外层节点的 toString 频繁依赖内层节点的反复转化,产生大量冗余开销。后续我在各节点类中增加缓存字段,使重复转化降为 O(1) 复杂度。
  2. 状压DP未记忆化:在为中、小规模多项式引入状态压缩DP进行乘开优化时,起初未对中间状态使用备忘录,导致相同子集划分被重复计算引发TLE,补充缓存机制后性能恢复正常。

优化引发的逻辑Bug与复杂度分析

为了将底层 DP 的拆分结果向上层组合,同时避免重新生成 AST 带来的代码臃肿,我尝试直接截获底层返回的字符串进行判断。初版实现中,我未加入 !ans.contains("*e") 等判定条件,导致在触发乘开优化后,外层少加了必要的包裹括号,直接破坏了表达式的等价性。

修复该问题时,我采用了 !ans.contains("*e") 进行特征嗅探。但在互测中被发现,当测试数据合法包含乘法与自然底数时(例如 exp((exp((2*exp(x)+3))))),内层的 2*exp(x) 会导致嗅探器发生误判,错误地处理了外层括号。

对比分析代码复杂度,未出现 Bug 的基础方法(如 expr.Term.toPoly)圈复杂度仅为 2-3,而承载优化逻辑的 Poly.greedyCompressPoly.buildExp 最大圈复杂度(OCmax)分别达到 11 和 14。单纯的字符串包含匹配将树状层级关系压缩为了线性匹配,丢失了AST的深度信息,导致方法在遇到exp嵌套可乘开exp单因子的时候括号层数变多,性能下降。

为降低复杂度并彻底解决问题,我引入了带有深度的单向扫描策略:

int index = 0;
for (int i = 0; i < ans.length() - 1; i++) {
    char c = ans.charAt(i);
    if (c == '(') { index++; }
    if (c == ')') { index--; }
    if (index == 0 && c == '*' && ans.charAt(i + 1) == 'e') {
        isFactor = false;
        break;
    }
}

测试策略与有效性

在互测阶段,我主要通过自身的bug来构造测试用例:针对架构缺陷的定向测试:阅读他人代码时,重点关注其函数的解析方式与缓存机制,若发现对方采用全局字符串替换处理函数,便构造包含大量冗余分支的选择因子嵌套数据,测试其短路特性以诱发 TLE;若发现未见明显缓存,便构造深层级的exp(exp(...))嵌套数据测试其时间复杂度。

AI使用策略

在三次迭代中,我会将自己的架构抛给AI,并询问其改进意见,比如第一次作业中,多项式因子类是应该单独抽象出一个类还是复用expr等。另外,针对一些特定的优化算法,我如果不会实现,也不会照搬AI源码,而是学习构建思路以后自己重新复现一遍,因此并非出现直接使用AI源码的情况。

除作业本身,在第一次和第二次作业的互测环节我均使用AI搭建了评测机,但是AI的数据构建效果并不好,不能有效地构造出bug。

心得体会

经过本单元的学习,我深切体会到了递归方法的巧妙性和危险性,以后涉及递归使用的方法,我会注意其边界条件,并在必要时添加缓存以减少不必要的时间开销。另外,我意识到优化可能伴随一定的风险,每一步优化都要全面考虑其与本身架构的适配性。

未来方向

希望辅导书能多给出一些架构提示和算法提示,鼓励同学进行合理的优化,并提醒同学一定要注意复杂度

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

309

社区成员

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

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