OO第一单元总结
1 第一次作业
第一次作业的要求概括来说就是解析表达式,展开括号,化简表达式,下面我分别介绍我的思想历程和最终解决方案。
1.1 解析表达式
根据课程作业文档,可以选用递归下降法和正则表达式解析,因为递归下降法方便操作并且具有很好的可扩展性,我最终选用递归下降法来解析表达式。
递归下降法的核心有两部分,即词法解析器(Lexer)和语法解析器(Parser),当我们在解析一个复杂的表达式时,首先要分析出表达式的所有的最小单元,然后编写Lexer来解析出表达式中的每个最小单元(Token),那么递归下降法的首要工作便做完了,然后就要进行语法解析了,语法解析器的编写要严格按照文法,它的本质即是将表达式依据文法格式来递归向下搜索,最后生成一个树的结构。当获得了表达式的每一个文法部分后,我们就可以对每一种文法设计处理逻辑来进行下一步工作。
1.2 展开括号
我在处理这一步时遇到了较大的困难,困难聚焦于如何处理表达式因子后的指数,或者说如何将表达式和表达式因子结合起来,我在开始想要直接只设置一个表达式类,在其中设置一个专门的指数,然后很快我就发现这种表达的繁琐,因为这样设置意味着就算是一个表达式,我也要额外设置指数1,并且在处理括号展开时需要将表达式中的项的相加和表达式的乘方运算结合起来,增加了实现难度,最后我放弃了这种架构,我选择将表达式和表达式因子分隔开,其中表达式因子继承于抽象类Factor。
在细节的处理方面我学习前人的经验,根据表达式化简到最后总是能表示为a*x^b的和,我设置了一个基本格式类来表达每一个基本部分的系数和指数,然后将表达式类、项类、因子类都实现一个到基本类转变的方法,最后我们便可以得到一组基本表达式,实际上只要输出便满足了作业要求,但是因为性能分数,我们大概还要进行下一步。
1.3 化简表达式
我在化简表达式时的核心操作只有一步,即合并同类项,遍历得到的基础表达式并且将指数相同的项的系数相加。然后就是一些繁琐的表达式化简操作了,例如处理系数为0的项、将正项提到前面、处理指数为0或1、处理系数为1或-1等等,但是在性能优化的同时也会增加bug发生几率,我在第二次作业中就因为优化发生了bug。
2 第二次作业
第二次作业中增加了多层括号嵌套和指数函数和函数表达式。其中多层括号嵌套如果第一次作业中是递归下降法实现的话就可以直接解析多层括号嵌套了。下面我总结一下我如何处理指数函数和函数表达式的。
2.1 指数函数
增加指数函数对架构最大的影响在于最简式不同了,但是整体架构和思路还是和第一次作业相同。现在最简式可以写为
a
∗
x
b
∗
e
x
p
(
a
1
∗
x
b
1
+
a
2
∗
x
b
2
∗
e
x
p
(
.
.
.
)
+
.
.
.
.
.
.
)
a*x^b*exp(a1*x^b1+a2*x^b2*exp(...)+......)
a∗xb∗exp(a1∗xb1+a2∗xb2∗exp(...)+......)
的形式,所以我选择在最简项类中增加一个ArrayList来存放exp的指数部分。然后我遇到了主要难点为如何合并同类项,即如何比较exp的指数部分相同。我选择的方法是先比较x的指数是否相同,再比较exp是否为空,最后比较exp的每一项,递归调用。
在增加了指数函数后就有了很大的优化难点(如果想要优化的最好),我最后实现了提取最大公因数优化方式和exp的一些特殊情况的优化,例如exp((2*x))优化为exp(x)^2。
2.2 函数表达式
我在处理函数表示式时使用了偷懒的办法,即我选择在字符串层面对函数表达式进行替换,然后就可以正常处理其他部分。不过在替换时还是可能会有很多bug,例如:1、要先替换x变量;2、要先将exp中x替换掉;3、在寻找函数中的实参时要注意有可能还有函数并且要注意寻找的终止条件。
2.3 Bug修复
在第二次作业中,我在互测中被发现了一个bug,bug出现的地方在exp的优化部分,出现的原因是我在想要把例如exp((3*x))优化为exp(x)^3时直接将x的系数设置为了1,这导致在下一次用到这个exp时就只输出exp(x)。这个bug出现的经历让我更加深刻地认识到了两件事,1:不要在类的外部去修改类的内部变量,即不要在类的内部随便设置可以改变类的核心数据的方法,这不仅会增加bug出现的几率,而且不符合面向对象的低耦合、高内聚要求。2:在进行最后的长度优化时,因为优化的逻辑往往是并列的,这会增加更多的分支,增加了代码的复杂度,也增加了bug的出现概率。经过这样的思考,我在后面的设计中避免了从外部修改类内部变量,并且将优化的逻辑拆分成几个并列的层次,降低了最后输出方法的臃肿。
3 第三次作业
第三次作业只增加了函数表达式中的函数调用和求导因子,整体的代码量是较少的。其中前一个需求本质上并没有发生改变,我的第二次架构可以直接实现。
3.1 求导因子
求导因子的实现也较为简单,我先将求导因子中的表达式转换为基本项形式,然后在基本项中实现求导运算。即
d
x
(
a
∗
x
b
∗
e
x
p
(
.
.
.
)
)
=
a
∗
b
∗
x
(
b
−
1
)
∗
e
x
p
(
.
.
.
)
+
a
∗
x
b
∗
d
x
(
.
.
.
)
∗
e
x
p
(
.
.
.
)
dx(a*x^b*exp(...))=a*b*x^(b-1)*exp(...)+a*x^b*dx(...)*exp(...)
dx(a∗xb∗exp(...))=a∗b∗x(b−1)∗exp(...)+a∗xb∗dx(...)∗exp(...)
其中(…)表示exp的指数部分,然后在求导时注意x的指数是否为0就可以了。
4 架构设计与可扩展性
我的架构设计如上所述。其中,Token、Parser、Lexer是解析输入用的,Func类是函数调用,Factor类有五种,从左到右分别为指数函数因子、表达式因子、变量因子、求导因子、数字因子,Norm类即为基本项、Norms类是基本项的集合。在可扩展性方面我认为还是比较良好的,在从第一次作业到第二次作业以及第三次作业,我的架构的框架并未改变,仅仅修改了原有的方法和变量并增加了新的方法和变量。例如,倘若再增加三角函数的要求,我大致能想到的扩展方向为:1、增加三角函数类,其中会实现到基本项的方法。2、在基本项中增加新的存放三角函数中的基本项的ArrayList。3、在基本项中改写加、乘、乘方等操作来满足三角函数要求,并且有可能增加三角函数的一些运算。4、最后改写输出逻辑。不过三角函数的难度较大,没有亲手操作可能会有漏洞,但是在这个架构上至少可以直接有扩展的方向。
五 复杂度分析
复杂度分析我采用Metrics插件,以下为分析结果。可以看出在函数调用、Lexer类、Norm和Norms类中有数个方法复杂度超标,而我的bug也的确方式在Norm类中超标的输出逻辑方法中,可见在以后作业中我要充分地控制方法的复杂度,尽量将方法逻辑分明地拆开。
方法 | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Derivation.Derivation(Expr) | 0.0 | 1.0 | 1.0 | 1.0 |
Derivation.toNorms() | 0.0 | 1.0 | 1.0 | 1.0 |
Expr.Expr(ArrayList) | 0.0 | 1.0 | 1.0 | 1.0 |
Expr.toNorms() | 1.0 | 1.0 | 2.0 | 2.0 |
Exprfactor.Exprfactor(Expr, BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Exprfactor.toNorms() | 0.0 | 1.0 | 1.0 | 1.0 |
Factor.toNorms() | 0.0 | 1.0 | 1.0 | 1.0 |
Func.addFunc(String) | 4.0 | 1.0 | 5.0 | 5.0 |
Func.callFunc(char, ArrayList) | 21.0 | 3.0 | 9.0 | 10.0 |
Func.existFunc() | 4.0 | 3.0 | 4.0 | 5.0 |
Func.exprFin(String) | 3.0 | 1.0 | 2.0 | 3.0 |
Func.Func() | 0.0 | 1.0 | 1.0 | 1.0 |
Func.funcSimplify() | 1.0 | 1.0 | 2.0 | 2.0 |
Func.parserFunc() | 2.0 | 1.0 | 3.0 | 4.0 |
Func.parserParameter() | 33.0 | 7.0 | 11.0 | 17.0 |
Lexer.Lexer(String) | 20.0 | 13.0 | 12.0 | 14.0 |
Lexer.move() | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.notEnd() | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.now() | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.simplify(String) | 20.0 | 1.0 | 9.0 | 9.0 |
Main.main(String[]) | 3.0 | 1.0 | 3.0 | 3.0 |
Norm.addCoefficient(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Norm.change() | 0.0 | 1.0 | 1.0 | 1.0 |
Norm.getCoefficient() | 0.0 | 1.0 | 1.0 | 1.0 |
Norm.getIndex() | 0.0 | 1.0 | 1.0 | 1.0 |
Norm.getPowerFuncNorms() | 0.0 | 1.0 | 1.0 | 1.0 |
Norm.isPowAddP() | 13.0 | 5.0 | 5.0 | 5.0 |
Norm.Norm(BigInteger, BigInteger, Norms) | 0.0 | 1.0 | 1.0 | 1.0 |
Norm.putPow() | 20.0 | 1.0 | 6.0 | 6.0 |
Norm.setCoefficient(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Norm.simPow() | 4.0 | 2.0 | 4.0 | 4.0 |
Norm.toOut() | 25.0 | 1.0 | 14.0 | 14.0 |
Norms.addNorms(Norms) | 1.0 | 1.0 | 2.0 | 2.0 |
Norms.changeNorms(Norm) | 0.0 | 1.0 | 1.0 | 1.0 |
Norms.deleteZero(Norms) | 17.0 | 1.0 | 6.0 | 8.0 |
Norms.derNorm(Norm) | 14.0 | 4.0 | 6.0 | 6.0 |
Norms.derNorms() | 3.0 | 1.0 | 3.0 | 3.0 |
Norms.expEqual(Norms, Norms) | 34.0 | 9.0 | 10.0 | 15.0 |
Norms.getNorms() | 0.0 | 1.0 | 1.0 | 1.0 |
Norms.mergeNorm() | 18.0 | 1.0 | 8.0 | 9.0 |
Norms.minus() | 1.0 | 1.0 | 2.0 | 2.0 |
Norms.mulNorms(Norms) | 3.0 | 1.0 | 3.0 | 3.0 |
Norms.normEqual(Norm, Norm) | 6.0 | 3.0 | 4.0 | 4.0 |
Norms.Norms() | 0.0 | 1.0 | 1.0 | 1.0 |
Norms.powNorms(BigInteger) | 23.0 | 1.0 | 11.0 | 11.0 |
Norms.toOut() | 19.0 | 1.0 | 10.0 | 11.0 |
Num.Num(String) | 0.0 | 1.0 | 1.0 | 1.0 |
Num.toNorms() | 0.0 | 1.0 | 1.0 | 1.0 |
Parser.Parser(Lexer) | 0.0 | 1.0 | 1.0 | 1.0 |
Parser.parserDerivation() | 0.0 | 1.0 | 1.0 | 1.0 |
Parser.parserExpr() | 3.0 | 1.0 | 4.0 | 4.0 |
Parser.parserExprfactor() | 4.0 | 2.0 | 4.0 | 4.0 |
Parser.parserFactor() | 7.0 | 5.0 | 7.0 | 7.0 |
Parser.parserNum() | 3.0 | 2.0 | 3.0 | 3.0 |
Parser.parserPowerFunc() | 5.0 | 2.0 | 5.0 | 5.0 |
Parser.parserTerm() | 5.0 | 1.0 | 6.0 | 6.0 |
Parser.parserVariable() | 6.0 | 2.0 | 6.0 | 6.0 |
PowerFunc.PowerFunc(Factor, BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
PowerFunc.toNorms() | 2.0 | 1.0 | 3.0 | 3.0 |
Term.Term(ArrayList, Boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
Term.toNorms() | 2.0 | 1.0 | 3.0 | 3.0 |
Token.getContent() | 0.0 | 1.0 | 1.0 | 1.0 |
Token.getType() | 0.0 | 1.0 | 1.0 | 1.0 |
Token.Token(Type, String) | 0.0 | 1.0 | 1.0 | 1.0 |
Token.toString() | 0.0 | 1.0 | 1.0 | 1.0 |
Variable.toNorms() | 0.0 | 1.0 | 1.0 | 1.0 |
Variable.Variable(String, BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Total | 350.0 | 115.0 | 229.0 | 250.0 |
Average | 5.223880597014926 | 1.7164179104477613 | 3.417910447761194 | 3.7313432835820897 |
六 hack策略
我在互测hack中主要还是依托自动评测机。经过我对hack成功数据的观察,我发现了一些容易被hack的点:1、exp((-x))输出错误,这可能是由于在处理输出优化时而犯的错误。
2、多层exp嵌套求导而tle,这可能是过度调用toString方法或者不恰当的处理逻辑导致的。
七 心得体会
依托自动评测机。经过我对hack成功数据的观察,我发现了一些容易被hack的点:1、exp((-x))输出错误,这可能是由于在处理输出优化时而犯的错误。
2、多层exp嵌套求导而tle,这可能是过度调用toString方法或者不恰当的处理逻辑导致的。
七 心得体会
经过这三次迭代,我更直观的认识到了面向对象理论课中的一些思想。例如,1:低耦合高内聚。只能说我在这一方面吃亏后才深深地领会了这一思想。2、层次化架构。在设计这一次作业时不能仅仅将眼光停留在这次,倘若没有清晰的层次的架构在下一次作业中就可能会有更大的工作量,而层次化架构不仅提高可扩展性、增加可读性,也降低了错误发生几率。至于未来方向,作为一个学习者,我并没有什么高瞻远瞩的意见可以分享。