2026 OO 第一单元总结

张曜麟-24231218 2026-03-28 23:28:44

第一单元总结

基于度量的程序结构分析

Method metrics

MethodCogCev(G)iv(G)v(G)
DoubleIndex.DoubleIndex(Poly, BigInteger, BigInteger)0.01.01.01.0
DoubleIndex.equals(Object)5.06.01.06.0
DoubleIndex.getExpIndex()0.01.01.01.0
DoubleIndex.getXIndex()0.01.01.01.0
DoubleIndex.getYIndex()0.01.01.01.0
DoubleIndex.hashCode()0.01.01.01.0
DoubleIndex.plus(DoubleIndex, DoubleIndex)0.01.01.01.0
DxFactor.DxFactor(Expr)0.01.01.01.0
DxFactor.factorToPoly()0.01.01.01.0
DyFactor.DyFactor(Expr)0.01.01.01.0
DyFactor.factorToPoly()0.01.01.01.0
ExpFactor.ExpFactor(Factor, BigInteger)0.01.01.01.0
ExpFactor.factorToPoly()0.01.01.01.0
Expr.addTerm(Term)0.01.01.01.0
Expr.clone(Expr)0.01.01.01.0
Expr.Expr()0.01.01.01.0
Expr.exprToPoly()1.01.02.02.0
ExprFactor.changeIndexTo(BigInteger)0.01.01.01.0
ExprFactor.ExprFactor()0.01.01.01.0
ExprFactor.factorToPoly()0.01.01.01.0
FuncFactor.factorToPoly()0.01.01.01.0
FuncFactor.FuncFactor(Factor)0.01.01.01.0
Function.Function()0.01.01.01.0
Function.getInstance()0.01.01.01.0
Function.init(String)0.01.01.01.0
Function.toPoly()0.01.01.01.0
GradFactor.factorToPoly()0.01.01.01.0
GradFactor.GradFactor(Expr)0.01.01.01.0
Lexer.getNumber()2.01.03.03.0
Lexer.Lexer(String)0.01.01.01.0
Lexer.next()6.02.05.06.0
Lexer.next(int)1.01.02.02.0
Lexer.peek()0.01.01.01.0
MainClass.getInput(Scanner)0.01.01.01.0
MainClass.main(String[])2.01.03.03.0
NumFactor.factorToPoly()0.01.01.01.0
NumFactor.NumFactor(BigInteger)0.01.01.01.0
Parser.getIndex()3.01.03.03.0
Parser.getSign()3.01.02.03.0
Parser.isPlusOrMinus(String)1.01.02.02.0
Parser.parseDxFactor()0.01.01.01.0
Parser.parseDyFactor()0.01.01.01.0
Parser.parseExpFactor()0.01.01.01.0
Parser.parseExpr()1.01.02.02.0
Parser.parseExprFactor()0.01.01.01.0
Parser.parseFactor()5.011.011.012.0
Parser.parseFuncFactor()0.01.01.01.0
Parser.parseGradFactor()0.01.01.01.0
Parser.parseNumFactor()4.01.03.04.0
Parser.parseOptionalFactor()2.02.01.02.0
Parser.Parser(Lexer)0.01.01.01.0
Parser.parseRecFactor()0.01.01.01.0
Parser.parseTerm()4.01.04.04.0
Parser.parseXFactor()0.01.01.01.0
Parser.parseYFactor()0.01.01.01.0
Poly.bestExp(Poly)1.02.01.02.0
Poly.devide(BigInteger)1.01.02.02.0
Poly.dx()3.01.03.03.0
Poly.dy()3.01.03.03.0
Poly.equals(Object)2.03.01.03.0
Poly.expVersion1(Poly)2.01.03.03.0
Poly.expVersion2(Poly)2.01.03.03.0
Poly.getGcd()1.01.02.02.0
Poly.grad()0.01.01.01.0
Poly.hashCode()0.01.01.01.0
Poly.isFactor()6.03.01.07.0
Poly.monoToString(DoubleIndex, BigInteger)28.01.011.014.0
Poly.mul(Poly, Poly)9.01.05.05.0
Poly.plus(Poly, Poly)10.01.06.06.0
Poly.Poly(DoubleIndex, BigInteger)1.01.02.02.0
Poly.pow(Poly, BigInteger)5.03.03.04.0
Poly.replace(Poly)1.01.02.02.0
Poly.toString()9.06.04.07.0
RecFactor.factorToPoly()0.01.01.01.0
RecFactor.RecFactor(int, Factor)0.01.01.01.0
Recursion.add(String)0.01.01.01.0
Recursion.addRule(String)1.01.02.02.0
Recursion.get(int)0.01.01.01.0
Recursion.getInstance()0.01.01.01.0
Recursion.Recursion()0.01.01.01.0
Term.addFactor(Factor)0.01.01.01.0
Term.Term()0.01.01.01.0
Term.termToPoly()1.01.02.02.0
XFactor.factorToPoly()0.01.01.01.0
XFactor.XFactor(BigInteger)0.01.01.01.0
YFactor.factorToPoly()0.01.01.01.0
YFactor.YFactor(BigInteger)0.01.01.01.0
Total126.0116.0155.0181.0
Average1.44827586206896551.33333333333333331.78160919540229882.0804597701149423

分析

平均圈复杂度(v(G)):2.08,处于较低水平,表明大部分方法控制流简单,易于测试。平均认知复杂度(CogC):1.45,同样较低,说明大多数方法直观易懂。但存在少数高复杂度方法,显著拉高了平均值。

大多数方法 iv(G) ≈ v(G),表明模块间调用复杂度和方法自身控制流复杂度基本一致,没有明显的过度耦合。

  • Poly.monoToString 的 iv(G)=11 而 ev(G)=1,说明该方法内部有大量分支但结构清晰(非结构化程度低),认知复杂度高主要源于分支数量而非结构混乱,将其拆分为多个小方法可能会比较好。
  • Poly 类:多个方法复杂度较高(mulplustoStringmonoToStringisFactor),说明 Poly 可能存在职责过重
  • Parser.parseFactor 圈复杂度过高,分支过多,可以考虑应用工厂模式或策略模式。

架构

img

设计考虑

  • MainClass:处理输入,调用ParserLexer,尽量避免在MainClass里塞太多东西。
  • Lexer:词法分析,拆出Token。
  • Parser:语法分析,递归下降解析表达式、项、因子。
  • Expr:表达式,管理下一层的项。
  • Term:项,管理下一层的因子。
  • Factor:因子接口,使各种因子能被统一管理、统一转为多项式。
  • ExprFactor:表达式因子,继承了Expr,搭载了Factor接口,并记录指数。
  • XFactor/YFactor:幂函数因子,记录指数。
  • ExpFactor:指数函数因子,记录其内因子指数和外常熟指数。
  • Function:用单例模式管理初始化形参下的非递推函数的多项式。
  • FuncFactor:非递推函数因子,记录实参因子,toPoly时调用Function完成替换。
  • Recursion:用单例模式初始化并管理0-5这六个递推函数。
  • RecFactor:递推函数因子,记录序号和实参因子,toPoly时调用Recursion完成替换。
  • DxFactor/DyFactor/GradFactor:求导因子,记录被求导的表达式。
  • NumFactor:常数因子。
  • DoubleIndex:将x的指数、y的指数、exp的指数统合在一起。
  • Poly:将多项式的每一项以<DoubleIndex, coef>的键值对形式用HashMap管理,提供多项式加法、多项式减法、多项式乘法、多项式快速幂、多项式求导、多项式化字符串等方法。

优点

  1. Factor 接口统一了各类因子,使表达式构建和多项式转换能够以一致的方式处理,便于后续扩展新的因子类型。
  2. Poly 类将多项式表示为 HashMap<DoubleIndex, BigInteger>,封装了加、乘、幂、求导、替换等运算,使得符号计算的核心逻辑集中且可复用。
  3. Lexer 负责词法分析,Parser 负责语法分析,职责清晰,为复杂表达式提供了可维护的解析机制。
  4. Function 和 Recursion 采用单例模式,保证全局只有一个函数定义和递归定义集合,符合“整个程序只有一组函数定义和递归规则”的情况。

缺点

  1. Poly.mul 和 Poly.plus 每次运算都创建新的 HashMap,且频繁进行 equals 和 containsKey 判断,时间复杂度高。
  2. DoubleIndex 中包含一个 Poly expIndex,形成了“指数中的多项式”的嵌套结构,导致多项式的表示极度复杂。这种设计使得求导、替换、字符串输出等操作都需要递归处理,极易引入 bug,且难以调试。
  3. 耦合度过高,Parser 直接依赖所有具体的 Factor 实现类,并显式调用其构造函数。可以考虑通过工厂模式或反射来降低耦合。
  4. 代码重复,DxFactorDyFactorGradFactor 的结构几乎完全相同。

架构设计体验

没有重构。刚开始的时候想过要不要把每个返回值都变成多项式的形式,这样就能少开几个类。最后还是决定老老实实那类暂存,最后再转为多项式,因为担心会加其他操作。这个架构虽然麻烦不少,但拓展起来还是没让我有什么负担。基本只要增加Factor旗下的因子种类就行,不需要对已有的内容作很大变动。不过还是有些地方写的不够精致,实现得比较偷懒,导致不得不修改。

  • 第一次作业时,用Lexer分析词法,Parser分析语法,将原式递归下降地剖分成表达式、项、因子。出于单一职责原理,我将分析与计算分开,设计Poly类承担计算、输出的职责,为表达式、项、因子设计toPoly方法。
  • 第二次作业时,新增了指数函数因子、选择式因子和自定义函数因子。指数函数因子对架构的影响最大,因为多项式的结构改变了。最后还是将新的单项式表示成了<指数,系数>的形式,与原来的表示方法统一,避免对Poly类进行太大的变动。选择式因子和自定义函数因子只需要增加新的类即可,但是偷懒用字符串替换处理自定义函数因子,没有将分析与计算彻底分离,在互测中吃了苦头。Bug修复时在Poly类中新增了replace方法,将实参替换的步骤后移。
  • 第三次作业时,新增了求导因子和自定义递推函数因子。前者虽然指导书上建议分层求导,但我觉得还是直接对多项式求导比较简洁,只需要在Poly类中增加求导方法即可,因为多项式的格式统一,所以很好实现。对于后者,我突发奇想地将自定义函数因子用单例模式来处理,这样就能避免冗余的传参。

新的迭代情景

增加sin, cos函数因子。

在我的最终设计中,多项式结构再次被颠覆。考虑将单项式表示成$ax^bexp(c)*\Pi_isin(poly_i)^{biginteger_i}*\Pi_jcos(poly_j)^{biginteger_j}$的形式,用HashMap<Poly, BigInteger>来代表三角函数部分,合并到(b,c)中共同构成单项式的键。然后需要修改多项式乘法、多项式化字符串等方法。

遇到的bug

  1. 第二次作业的某个样例使我的程序发生了NullPointerException错误原因:Poly类和DoubleIndex类定义了三个静态常量Poly.ZERO, Poly.ONE, DoubleIndex.ZERO,这三个量的初值存在循环依赖。由于JVM类加载初始化阶段的特性,如果先访问DoubleIndex.ZERO,那么DoubleIndex的初始化将被中断,直到Poly的初始化完毕才会继续,所以Poly.ONE将会以DoubleIndex.ZERO在准备阶段被赋上的默认值null进行初始化,导致NullPointerException。对策:避免循环依赖。
  2. 第二次作业中测数据[(exp(0) == 0)? [(exp((x+exp(x))) == exp((exp(x) + x))) ?2:009] : -1],我的程序输出为1原因:第一次作业中我将正负号视为+1, -1因子,在parseTerm阶段处理了所有的正负号,因为parseFactor只会被parseTerm调用,所以parseFactor不需要处理正负号。但第二次作业中指数因子、函数因子都会越过parseTerm直接调用parseFactor,所以负号没被处理。位置:Parser类的parseFactor方法对策:在parseFactor中的parseNumFactor处增加了判断符号的功能。注意因子中只有常数因子可能带符号,且最多一个符号。
  3. 第三次作业中自己构建的数据exp(((9+x)*(9+y)*(9+x^2)*(9+y^2)*(9+x^4)*(9+y^4)*(9+x^8)*(9+y^8)*(9+exp(x)+exp(y))*(9+exp(x^2))))让我的程序超时。原因:Poly类中pow方法中的快速幂实现得不够精细,快速幂中当指数归为0时应该及时退出,而不是再执行一次多余的a=a*a,如果执行了,将会导致a比最终的ans大,就可能出现ans堪堪合法而最终的a过大的情况。位置:Poly类的pow方法对策:b右移到0时及时退出。
  4. 第二次作业的选择式因子断路使我的程序超时。原因:选择式因子的cost不会带上没被选择的因子,所以这个因子的解析复杂度可能很高。如果用字符串替换来处理函数因子,则会导致超时。位置:Parser类的parseFactor方法对策:将字符串替换改为先用类存储信息,最后再进行多项式替换。

bug总结

很多问题出现在了Parser类的parseFactor方法和Poly类中。parseFactor的ev, iv, v值均很高,表明这个方法的复杂度、耦合度过高。Poly类的OC, WMC较高。这表明圈复杂度过高的部分可能更加容易发生错误,因为逻辑复杂度过高会让编写和检查变得困难。

测试策略

黑箱测试:分别用自己构造的边界数据和评测机进行测试。评测机效果一般,边界数据效果很好,上一次作业中被别人hack的数据也可以借来用在本次互测里,毕竟很多bug出现率很高。

白箱测试:检查代码具体内容,尤其是一些关键的类和方法。

优化

提取出exp内多项式各项系数的gcd到exp()外作为指数,与不加优化的版本比较后择优。

这种情况因为可能使同一个多项式实例被多次调用toString()方法,所以用记忆化搜索来优化了时间复杂度。

本来想以最大质因子来对系数进行分组,但是最大质因子不好计算,遂作罢。

我觉得过于在意性能分可能会把代码搞得很复杂,影响正确性和效率。

大模型相关使用

第二次作业开始,我将多项式的每一项表示为a*x^b*exp(c)的形式,并以键值对<(b, c), a>的形式用HashMap管理。(b, c)的部分我新开了一个类进行管理,这就需要对这个类进行哈希化,于是我询问了LLM自定义类的哈希应该如何实现,效果不错。

因为对正则表达式不够熟练,所以涉及字符串替换的部分也大多让LLM来写的。

此外,还让大模型来分析并检查互测代码、生成评测机,大模型展现出了笨拙的一面,但还是提供了不少帮助。比如说他虽然因为总是搞错题目限制而在找bug上帮不上什么忙,但是在分析程序架构和简单的搜索生成任务上表现得不错。

没感觉到有完完全全是AI生成的代码。

心得体会

一开始抵触互测,现在觉得互测还是挺有趣且有益的。我在强测中几乎没有出错,也就是说如果没有互测的集思广益,那么我代码中的bug可能就永不见天日了。

阅读别人的代码也不只是挑刺,我学习到了很多优美的写法,在迭代中用到了自己的代码里。

未来方向

选择式因子这种小巧思的设计我挺喜欢的,希望之后能多多设计出这样精巧的规则。

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

309

社区成员

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

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