第一次作业
UML类图
反思:PowFactor
、NumFactor
、ExprFactor
的父类是一个只有指数的Factor
,感觉怪怪的,更应该把他们继承自同一个接口来实现统一的toPoly方法。
架构分析
第一次作业需要实现从0到1的构建,我们需要对数学意义上的表达式结构进行建模并完成单变量多项式的括号展开,故主要任务有:
task1:确立关键性的类并构建他们之间的关系
指导书清晰地指出了三个层次的概念:因子(变量因子、常数因子、表达式因子)、项、表达式,我们只需要为他们分别建类(Factor
、Term
、Expr
),并把Factor
作为PowFactor
、NumFactor
、ExprFactor
的父类。同时注意表达式->项->因子这样的层次关系,依此完善这三个关键类的属性和方法。
学长博客里给项一个sign的属性好聪明
task2:在上述的基础上完成各个类的解析–递归下降法
递归下降的内容在oolens推送的基础上修改完善
-
词法分析
Lexer
Lexer
在遍历预处理之后的字符串时,识别其中的语法单元加入tokens中。(Token
这一枚举类型用于记录表达式的基本语法单元)。在获得到的tokens基础上进行接下来的语法分析。 -
语法分析
Parser
对表达式按照规则进行拆分,识别出构成表达式的一个一个项;对项按照规则进行拆分,识别出构成表达式的一个一个因子;构建自顶向下的语法树。
task3:计算化简表达式
将a*x^b作为运算的最小单元用Moon
存储;Poly
由Moon
组成,且拥有多项式相加、多项式相乘、多项式求幂的方法用于计算;为所有的关键类都增加将其转换为Poly
的方法。
PreTreat
用于预处理,对输入的字符串删除空白字符、合并±、去除指数前的+号。至此,第一次作业UML图中所有类的功能都已介绍完毕,之后的作业在此基础上迭代。
输出优化:在转化为字符串的过程中,处理了指数为0、1,系数为+1、-1、0,正项前移的情况。
oo度量
大多数方法的复杂度在合理范围之内,仅给出复杂度超标的几个方法:
-
Mono
类中toString()
方法生成单项式的字符串形式时,需要使用大量的if-else
语句判断优化。 -
Term
类中toPoly()
方法由于第一次作业将Factor
设置为父类而非接口,需要判断语句进行强制类型转换。 -
Lexer
类中使用if-else
对表达式的语法单元进行解析,因此结构非常冗杂,导致代码量暴增。
自测/公测/互测bug
课下在评测机上运行时,发现程序很容易超时(长久地陷入运行中且不输出结果),定位问题后发现是由于Poly
类中addPoly、mulPoly、powPoly等与Poly相关的计算方法没有及时合并/化简,而化简这一环节放在了最后调用simplePoly。故当时的解决方案是在每次Poly相关的计算后都调用一次simplePoly。
为什么我没有在addPoly的过程中就实现合并呢?嗯因为第一次作业写的时候呆得很,没有意识到可以在实现加法的时候边遍历边合并,最终就能够得到最简式;而是在simplePoly中根据x的幂次对monoList排序之后再次遍历进行的合并。
第二次作业
UML类图
架构分析
第二次作业的主要任务为:
task1:新增指数函数因子
为指数函数建类EFactor
,实现Factor
接口(将原先作为父类的Factor
修改为接口)。语法分析增加对指数函数因子的分析。
改用a*x^b*exp作为运算的最小单元,指数从int类型改用BigInteger存储。修改Poly
中的计算方法使之能够实现指数相关的运算。
hw2最终的框架对
Poly
进行了较大的修改。之前的计算在每一次计算完成返回结果时都需要调用simplePoly,每一次的调用实际上要遍历两次monoList(排序一次合并一次),笨笨的而且肯定会超时。在bug修复阶段,废弃simplePoly,修改addPoly在计算的同时合并使结果最简。
task2:新增自定义函数因子
为自定义函数因子建类FuncFactor
,属性包括实参替换后函数的字符串形式、实参替换后的Expr
形式、自定义函数因子的指数,实现Factor
接口。
新增Define
类,由HashMap<String, String[]>parasSet存储函数名称和形参,HashMap<String, String>funcSet存储函数名称和函数字符串形式;由方法addFunc向前述HashMap加入原函数,方法newFunc实现形参的替换。
语法分析增加对自定义函数因子的分析,借用Define
类的方法分析FuncFactor
相关属性。
输出优化:在转化为字符串的过程中,在第一次作业的基础上,求出了exp指数部分的最大公因数(由Mono
中getCoeListPo、gcd、ngcd方法实现);对exp指数部分只有一个变量因子/常数因子的情况少输出一层括号(由Mono
中checkFormat判断)。
oo度量
- 除第一次作业的一些高复杂度方法之外,
Poly
中addPoly的复杂度明显增加,addPoly需要实现带指数函数的合并化简,处理逻辑复杂,复杂度高也在情理之中。
自测/公测/互测bug
公测和互测环节出现的bug如下:
-
格式错误:形如exp((-x))的内部括号缺失
对exp指数部分只有一个变量因子/常数因子的情况判断过于混乱,逻辑发生了很大的错误。修复时重写了Mono
中checkFormat的判断。 -
数据范围:指数的类型为int没有更正为BigInteger,公测中下述样例错误
0
(((((((((((x^8)^8)^8)^8)^8)^8)^8)^8)^8)^8)^8)^8
- 运行时间:bug修复时将指数类型更改为BigInteger之后,由于BigInteger和int在参与运算时的运行速度差异,之前可以通过的部分数据点超时。
修复时起初认为可能是我在计算时频频调用simplePoly耗费较长时间,但在修改为边遍历边合并后运行时间并没有明显提升;后来sq老师告诉我在判断是否可以合并时不用每次都直接对两个Mono的exp做差,可以特判一下没有exp的情况。
第三次作业
UML类图
反思:在此次作业中,我也注意到hw2的代码在一些方法、属性上定义的不合理:有的方法虽然定义但从来没有被调用过;并不是所有的因子都会有指数项,SetExp和GetExp的方法不应该在接口上定义;我的设计中实际上Deriv
也是一种因子,应该去实现Factor
的接口,但是求导时需要传入define这个参数,所以想要更正的话,应该把Define
类中的成员和方法应该都设静态的,这样就可以通过类名直接调用,架构更加统一。(但是最后一次作业了毕竟x,只更正了前两个问题)
架构分析
第三次作业的主要任务为:
task1:新增求导算子
首先,为每一个关键类添加了求导的方法。之后,为了不影响Poly中的计算,在语法分析部分识别求导算子并返回求导后的结果(返回的结果为Factor
类型),之后正常参与运算即可。
task2:函数表达式中支持调用其他“已定义的”函数
第二次作业能够完成这项任务。
oo度量
- 将
Parser
中的parserFactor中的内部细节拆分为了parserPowFactor、parserNumFactor等方法,使得parserFactor的复杂度下降。 - 其他高复杂度的方法情况同第二次作业。
最终代码规模
自测/公测/互测bug
互测时被hack到的样例如下:
0
dx(exp(exp(exp(exp(exp(exp(exp(exp(x^2)))))))))
再次出现了超时的问题,主要是因为在从表达式向Poly转化的过程中,频繁调用powPoly方法(powPoly方法又需要调用mulPoly方法和addPoly方法),故修复时将指数为1的表达式因子直接返回底数而不再调用powPoly。(但是感觉每次对于超时问题的修改都不是长久之计)
可能的迭代场景:多变量
词法分析阶段设置
PowFactor
的底数;更改Mono
的属性,用HashMap存储变量的指数、系数;实现多变量Poly
的计算;求导时求偏导的实现。
hack策略
- 评测机
- 分别构造样例测试能否继续满足上次作业要求和本次新增作业(会着重关注新增要求的代码实现方式)
心得体会
- 由于频频出现超时的问题,虽然是一些很小的点,但是运行时间的差异让我具象地认识到需要频繁使用的变量及时存储而非频繁调用方法,在实现方法的时候要有意识地关注方式的时间复杂度和空间复杂度。
- 第一次作业oopre教的很多内容都有点忘记了,Java的一些语法知识和设计模式还需要更加深入了解。oolens公众号推文和实验课对推进完成作业的帮助很大。
- 讨论区的帖子和研讨课的内容都讲的特别特别好,不论是作业思路的分享还是对于代码编写约定的总结。前者在过程中给了很大程度思维上的启发,后者有一种临近ending的时候点明规律的豁然(尤其是自己画UML的时候会感叹,说的真对啊)。
- 让我们说:谢谢KPI结算大师。
未来方向
- 要求的测试数据对dx的限制好严格,不论是对dx数目的限制还是dx的cost的计算方式,可以再宽松一点点
- 可以增添一点点Java语法知识的渗透