309
社区成员
发帖
与我相关
我的任务
分享
OutputManager中展开项的方法以及对三目运算符的解析方法尤其高,说明嵌套深、可读性差,亟需重构。
OutputManager类较高,需要继续学习类的职责划分。OutputManager类较高。| Project | v(G)avg | v(G)tot |
|---|---|---|
| unit1 | 2.48 | 355 |

本单元笔者的架构是一以贯之的迭代产物,将整体架构分为三个部分:主控制流、表达式树、计算载体。
本单元的处理逻辑总体上可以分为四个部分:预处理、表达式解析、计算、优化输出。
Expr:储存表达式(因子),存储指数与项序列,项序列中存储表达式用+/-分割后的项。
Term:储存项,存储项符号(直接用Token统一处理)与因子序列,因子序列中存储用*分割的因子。
Factor:因子接口,需实现展开方法(toPoly)。
因子接口的设计原因:由于因子的逻辑层次较低但抽象层次较高,所以更适合设计成接口而非继承。
Variable:变量(幂函数)因子,存储变量名(String)、指数,便于后续迭代中可能的其他名字的变量。
Constant:常数因子,用BigInteger防止溢出
ExpFunc:指数函数因子,存储指数和实参因子(在“解析”步骤不做计算处理,直接存储表达式子树,减少不必要的时间开销)
Ternary:选择式因子,存储四个位置的实参因子
Func:非递推函数因子,存储实参因子
RecurFunc:递推函数因子,存储调用序号和实参因子
Derivative:求导因子,存储求导类型(直接存储TokenType统一处理)和内部实参因子
存储实参因子的处理方法:直接储存表达式子树,减少不必要的分支时间开销。
InputManager:工具类预处理输入字符串,得到格式统一、结构简单的便于统一处理的输入字符串。
InputData:存储预处理后的数据,包括函数个数,待解析字符串
Parser:文法解析器,解析来自Lexer的Token流,将表达式(或函数式)转化为
Lexer:词法解析器,将字符按照词法要求转化为Token流交予Parser处理
Token、TokenType:词元及词元类型
RecurDef:递推定义类,用于存储递推定义以便函数解析
Definer:函数定义工具类,用于存储所有函数的定义,便于解析使用,采用单例模式,避免频繁传参导致循环引用
预处理:接收控制台输入,根据输入的函数个数标识符存储相应数量的字符串。
函数解析:接收预处理后的数据的函数定义字段,将处理好的函数定义形参表达式存入函数定义工具类(单例)中
表达式解析:字符串->Token流->Expr表达式树->Poly多项式
优化输出
Poly类耦合,同时避免改写toString()重要方法Definer函数定义工具类单例,避免函数解析与调用过程中频繁传参存参导致循环引用等隐蔽问题f{i}(factor)时,若未解析f{i}(x)形参表达式,则先递推解析f{i}(x)形参表达式,之后取出形参表达式,将x替换为实参factor,解析成Poly后存储factor -> Poly的映射关系,下次遇到相同factor时直接取出解析后的值;在均衡性能和实现难度之后,采用的方式是,先解析出所有f{i}(x)形参表达式并缓存,调用时直接取出处理。Variable中有储存变量名,便于变量数量的扩展;在函数定义中保留函数名字段,便于函数种类的扩展。OutputManager复杂度较高:主要原因在于方法较冗长,可以做进一步拆分优化。Poly与Unit在职责划分中不够清晰:可以将Poly中计算的子方法(涉及单项式的)放在Unit中实现Trade off:出于对复杂度与代码可读性的考量,笔者没有做作业内容回顾:实现一个表达式解析器,解析单变量带幂次多层括号的表达式
架构设计:Parser、Lexer、Poly、Expr、Factor、Var、Constant、Processor
初步设计时,我采用的是所有表达式树相关节点都是基于Poly结构存储,在解析构建表达式树时同步计算,解析后直接输出为Poly,最终优化输出。
Unit单项式的定义(第一次作业):$a*x^i$
这种设计的优点是:思路直接,复杂度较低
这种设计的缺点是:未构建完整的表达式树;在解析分支时可能造成不必要的开销(着重体现在第二次作业的exp迭代中);没有做到“高内聚低耦合的设计思想”,职责划分不明确。

为提前准备第二次作业可能造成的架构问题,我在第二次作业开放前进行了项目的重构,并在第一次研讨课上分享了我对两种架构的理解,核心观点如下:
Poly和Unit类实现Parser和Lexer实现),再利用toPoly方法实现计算与展开第二次作业回顾:新增指数函数因子、选择表达式因子、函数调用因子
与第一次作业相比,笔者主要分步骤、分层次做了以下迭代工作:
exp因子增加:横向对比1、2次作业,笔者发现指数函数因子的添加是最基础与最核心的,因此,笔者选择优先单独迭代、单独测试指数函数因子。这一部分需要修改单项式与多项式的基础定义与计算方法。Unit的定义(第二次作业):$ax^iexp(Poly)$Definer:在本次作业,只需存储非递推函数的标识符到形参表达式的映射即可。Ternary类,存储解析后的四个因子的表达式子树,在计算解析(toPoly)时再判等解析,非必要分支过于臃肿的特定样例下若先解析再存储会导致TLE,然而重构后的架构“迫使”笔者新增因子类,构建表达式树之后再解析,间接规避掉这类TLE问题。debug章节笔者将详细分析。第三次作业回顾:支持双变量,新增求导因子,新增自定义递推函数
与第二次作业相比,笔者主要分步骤、分层次做了以下迭代工作:
双变量的迭代:横向对比2、3次作业,笔者发现双变量的添加是最基础与最核心的,因此,笔者选择优先单独迭代、单独测试指双变量相关内容。只需要扩充单项式的定义为$ax^iy^j*exp(Poly)$,并实现相关计算方法,值得注意的是,在括号添加判定中有一个隐蔽的bug,在debug章节,笔者将详细介绍求导因子的迭代:只需要增加新的因子类并在Poly类分别实现dx、dy、grad方法即可。factorA@factorA,定义为$h_AfactorA+h_BfactorB$,其中$h$为因子所在的表达式树的深度sin(factor)、cos(factor)整体处理逻辑和指数函数因子相差不大,主要难点在优化层面,涉及到众多三角函数公式的使用cost的前提下,放宽对递推函数深度的限制,考虑极端情况,若深度不限,则必须强制实现缓存甚至记忆化的处理。在第一单元的迭代过程中,笔者的代码有幸通过所有强测用例,并经受住所有hack,但在迭代的过程中我也遇到了和在互测中其他同学存在的bug,在下文一并分析
# 样例一
1
f(x)=x+2*x^2+3*x^3+4*x^4+8*x^8*exp(x)
[(x==1)?f(f(f(f(f(f(f(f(f(x))))))))):f((x+1))]
# 样例二
1
f(x)=exp(exp(exp(exp(exp(exp(x)^2)^2)^2)^2)^2)^2
0
f(f(f(f(x))))
# 样例三
0
0
exp((x*y))
样例一,如果选择先解析两个分支而不是先储存表达式树不解析,则会出现单个分支本不需要解析而解析导致的时间开销大的问题样例二,如果对是否需要深克隆理解不清导致全部深克隆会导致时间复杂度呈指数增长,其他诸如字符串替换,幂次运算不合理等问题也会导致TLE样例三(x因此加上y之后未修改,导致错误,这让笔者反思合理架构的重要性,因为这个问题本质是缺少正确的解耦。在本单元的作业中,我主要做了以下集中基础但不易出错的优化
exp((Poly))中合适的公因数提出HashMap自动实现合并同类项的操作笔者在本次作业正确性相关的部分中大概有10%左右的工作交予大模型完成,主要用于修改checkstyle时拆分方法与分支,涉及到的类是Parser和Lexer;在极少部分方法中笔者利用AI查询API与调整代码结构,使得单个方法更优雅美观;在优化过程中,笔者撰写方法时会将需求告诉大模型,让其辅助给出步骤或伪代码、以及可能用到的API,具体实现由人工完成;在完成并整体审查代码正确性后,笔者会让AI优化单个方法的实现逻辑,并利用AI检查代码可能出现的隐蔽错误与健壮性考量;同时,笔者在评测机或数据生成过程中利用AI辅助。
暂时无法确定互测房间内同学大量使用AI生成代码
在理论课中,老师建议我们自己设计代码框架,这也是面向对象的核心所在,因此,在三次作业的代码框架设计中,笔者完全自主实现,比较推荐的参考资料是OO加油站、实验课与练习代码。
本单元我们通过表达式解析迭代作业的训练,初步掌握了,面向对象的设计思想,理解了继承与接口的选择方法,对递归下降的输入解析方法有了更深刻的认知。在本单元的学习过程中,我参与了两次研讨课的分享活动,对代码架构的设计选择有了更深刻的理解,其中让我印象最深的是,老师在讲到类的设置的时候,提到有三种职责的类:关注输入输出、关注对象管理、关注流程控制,这让我对面向对象的思想有了更具体的体会。老师还提到了有关设计模式的内容,让我对下一单元的学习充满期待~加油!!!
可以增加更多高质量的资料分享,如OO加油站的高质量文章,更多的练习代码等