第一单元OO作业
本单元作业的主题是多项式展开与化简,既要将表达式进行展开,以去除不必要的括号,也要将展开的表达式进行合并,得到更精炼的输出以及更高的性能分。
第一次作业
本次作业需要完成的任务为:读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号后的表达式.
总体思路与架构设计
在第一次作业中,我使用了递归下降的方法,通过Lexer
类将输入进行处理,并将处理结果交给Parser
类,Parser
类将顺序结构的内容,建成Expr -> Term -> Factor -> Expr -> ···
的树状递归结构,再递归调用函数的toPoly()
方法,借助Poly
与Mono
两个类,将节点上形式不同的表达式树,建立为统一的表达方式。
具体细节
预处理
为了便于后续的处理,在Lexer
类的初始化中调用pre()
方法,将输入的字符串input
中的空白符删去,将连续的加减符号化为单一的符号。同时,考虑到表达式最开始会存在单独的-+
符号,所以在所有的表达式开始都添加了一个0
(但此操作也为后面操作带来一些问题如dx()
)。处理完后,便可以使用Parser类进行递归下降了。
private void pre() { input = input.replaceAll(" ", ""); input = input.replaceAll("\t", ""); input = input.replaceAll("\\^\\+", "^"); input = input.replaceAll("\\++", "+"); input = input.replaceAll("\\+-", "-"); input = input.replaceAll("-\\+", "-"); input = input.replaceAll("--", "+"); input = input.replaceAll("---", "-"); input = input.replaceAll("\\++", "+"); input = input.replaceAll("\\(\\+", "(0+"); input = input.replaceAll("\\(-", "(0-"); if (input.charAt(0) == '+' || input.charAt(0) == '-') { input = "0" + input; } }
递归下降
思想
递归下降,其实就是逻辑上一棵树的构建过程,并且将其按照不同的层次进行分拆的方法。
之前的数据结构课程中,进行表达式计算的时候,是采取两个栈对字符串进行处理,转换成后缀表达式,进行计算。而递归下降则是通过分析表达式的结构,将其从逻辑上分为表达式、项、因子这三种,只需要保证顺序调用即可。对于表达式来说,它只需要完成自己的方法,即将识别到的项加入到自己的terms
中。而term
如何识别,如何保证识别完term
后,下一个仍是符合term
表达形式的字符串,则是parseTerm()
方法的定义。这便是面向对象中的封闭性,从两种课程中对于大致相同的表达式计算而采取的不同思路方式,便是面向过程与面向对象的区别。
实现
在第一次作业中,写了三个方法,分别是parseExpr()
,parseTerm(int sign)
, parseFactor()
,内部结构都大致相同。但是由于每个项都有符号,所以在进入parseTerm()
时,将符号传入作为内部项的符号,从而外部表达式只需进行加法运算。parseFactor()
内部多了几条判断,来进行不同因子的识别,而根据形式化要求,部分因子会有指数,因此抽象出一个indexGet()
方法来获取此值。至于为什么时index而不是exp,因为他们都是`指数`
public Expr parseExpr() { Expr expr = new Expr(); expr.addTerm(parseTerm(1)); int sign; while ("+-".contains(lexer.peek())) { if (lexer.peek().equals("+")) { sign = 1; } else { sign = -1; } lexer.next(); expr.addTerm(parseTerm(sign)); } return expr; }
计算方法
经过递归下降的处理后,顺序的数据串,已经变成了有表达式、项、因子作为节点的树状结构。由于前三种抽象层次在计算方面的共性并不多,因此新建了两个类Poly
与Mono
,其中,Mono
类中,定义了运算时基本项的形式,即a*x^b
,Poly
则是Mono
的集合。这两个类中主要内容为实现计算的方法。
那么接下来的工作,便是将原来的结构变成新的类中的对象。Var
类和Const
类中比较简单,只需要将对应值填入即可。
//Var.java public Poly toPoly() { Poly poly = new Poly(); poly.monoAdd(new Mono(new BigInteger("1"), index)); return poly; }
其中monoAdd()
这一方法的实现需要注意,在加入一个新的mono
时,就进行合并
,判断两个基本项是否可加
,即x的指数
是否相同。这使得在多项式之间的加减时变的简单。
而Term类中便相对复杂一些,它需要将每个的factor的多项式进行相乘。Expr类中的也类似,只是将乘变成了加。
//Term.java public Poly toPoly() { Poly poly = new Poly(); for (Factor factor : this.factors) { poly = poly.multiPoly(factor.toPoly()); } if (this.sign == -1) { Const neg = new Const(new BigInteger("-1")); poly = poly.multiPoly(neg.toPoly()); } return poly; }
而multiPoly()
则是将两个多项式逐项相乘,成为新的多项式。
输出
由于在toPoly()
中就已经将其化简合并,输出只需要调用,Poly
与Mono
的重写的toString()
方法。而Poly
的toString
只需要遍历调用所有的mono
基本项的toString
进行拼接即可。
而Mono的toString也只需要进行不同情况的判断处理,注意好细节点,没有思维上的难度。
优化
合并同类项
如前文所说,在进行加减乘计算后,写入结果时,先判断结果中是否存在可合并的项,如果没有,再加入新的项。
调整输出
-
负数在第一位比正数在第一位会多出一个字符,因此我重写了
mono
的compareTo()
,以实现arraylist
的排序。 -
部分合理省略,在输出的过程中加入很多条件判断
-
-1*x^1 => -x
-
+32 => 32
-
x^0 => 1
-
bug分析
MemoryOverFlow
由于在计算时尝试全部拆开再合并,导致中间过程中MLE了。朴实无华的bug。解决措施也同样朴实,每次多项式的加减进行计算后,调用merge进行合并一下就ok了。
java中的内存,只要你失去了对他的指针,那么jvm就会帮你自动回收,不需要进行自己进行什么操作。
而且评测不会刻意卡memory的,只要不是全展开再合并或者说陷入了死循环,一般没有这个问题。
CurrentModificationException
使用ArrayList()
进行数据的存储时,可能经常出现CurrentModificationException
(这个bug甚至一度让我觉得以后再也不用ArrayList()了),一般来说,出现这个问题,表示在arraylist
的遍历过程中,通过一些其他的方法,对arraylist
中的元素进行了修改,常见的有以下两种原因。
深浅拷贝
因为浅拷贝的两个索引指向的是同一对象,因此很容易在遍历过程中因为修改了不应该修改的内容,导致出现错误。
只有八种基本类型的=
是深拷贝,其余的深拷贝均需自行实现。
调试与toString()
在意识到这个问题之前,我一直不知道调试的信息时如何产生在屏幕上的,感觉是理所应当的事情。但是在了解了之后,却觉得一切合乎情理。
在调试时,变量会调用toString
方法将将其内部值进行组织并打印出来,也就是说,如果你在toString
中修改了变量的值,那么在调试过程中进行遍历arraylist
就会出现CurrentModificationException
错误!!
解决措施
-
使用简单循环而非加强循环,但是这一操作只能解决互相修改导致的报错,但是仍会导致修改不该修改的,而导致更为隐藏的bug。
for (int i = 0; i < arraylist.size(); i++) {//替换for (Object object : arraylist){} Object object = arraylist.get(i); //... }
-
如果分不清楚某种情况下是否该用深拷贝,那就先使用深拷贝。
-
不使用
toString
而使用新定义的print
等函数。
方法度量
Method Metrics
Complexity metrics | 周四 | 21 3月 2024 16:52:09 CST | ||
---|---|---|---|---|
Method | CogC | ev(G) | iv(G) | v(G) |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.getNumber() | 2 | 1 | 3 | 3 |
Lexer.next() | 4 | 2 | 3 | 10 |
Lexer.peek() | 0 | 1 | 1 | 1 |
Lexer.pre() | 2 | 1 | 2 | 3 |
MainClass.main(String[]) | 0 | 1 | 1 | 1 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.indexGet() | 1 | 1 | 2 | 2 |
Parser.parseExpr() | 5 | 1 | 3 | 4 |
Parser.parseFactor() | 13 | 3 | 6 | 7 |
Parser.parseTerm(int) | 1 | 1 | 2 | 2 |
expr.Const.Const(BigInteger) | 0 | 1 | 1 | 1 |
expr.Const.setIndex(int) | 0 | 1 | 1 | 1 |
expr.Const.toPoly() | 1 | 1 | 2 | 2 |
expr.Expr.Expr() | 0 | 1 | 1 | 1 |
expr.Expr.addTerm(Term) | 0 | 1 | 1 | 1 |
expr.Expr.setIndex(int) | 0 | 1 | 1 | 1 |
expr.Expr.toPoly() | 2 | 1 | 3 | 3 |
expr.Mono.Mono(BigInteger, int) | 0 | 1 | 1 | 1 |
expr.Mono.addMono(Mono) | 1 | 1 | 2 | 2 |
expr.Mono.compareTo(Mono) | 0 | 1 | 1 | 1 |
expr.Mono.getCeo() | 0 | 1 | 1 | 1 |
expr.Mono.getIndex() | 0 | 1 | 1 | 1 |
expr.Mono.multiMono(Mono) | 0 | 1 | 1 | 1 |
expr.Mono.toString() | 14 | 2 | 8 | 8 |
expr.Poly.Poly() | 0 | 1 | 1 | 1 |
expr.Poly.addPoly(Poly) | 8 | 4 | 5 | 5 |
expr.Poly.merge() | 0 | 1 | 1 | 1 |
expr.Poly.monoAdd(Mono) | 3 | 3 | 3 | 3 |
expr.Poly.multiPoly(Poly) | 5 | 3 | 4 | 5 |
expr.Poly.sort() | 0 | 1 | 1 | 1 |
expr.Poly.toString() | 3 | 2 | 3 | 4 |
expr.Term.Term(int) | 0 | 1 | 1 | 1 |
expr.Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
expr.Term.toPoly() | 2 | 1 | 3 | 3 |
expr.Var.Var(int) | 0 | 1 | 1 | 1 |
expr.Var.toPoly() | 0 | 1 | 1 | 1 |
具体分析
第一次作业较为简单,用插件检查后并没有被标红的项,其中toString
中由于分支判断情况过多,因此略高。
parseFactor
方法过度耦合,在第二次作业中,将其拆开解耦,降低复杂度。
lexer.next()
条件过多,将字符逐个匹配修改为字符串中是否包含,减少条件判断的逻辑关系。
第二次作业
本次作业中需要完成的任务为:读入一系列自定义函数的定义以及一个包含幂函数、指数函数、自定义函数调用的表达式,输出恒等变形展开所有括号后的表达式。
总体思路与架构设计
本次作业新增了嵌套多层括号,指数函数因子,自定义函数因子。因此在上一次的架构进行了一些修改
-
增加
Definer
类与Fun
类来处理自定义函数。-
考虑到可能会在多个地方调用自定义函数解析,为了减少传参的麻烦,将
Definer
设置为static
类。 -
Fun
实现了Factor
的接口,报存函数表达式的具体内容以及形参,通过传入实参,处理后返回表达式的字符串。 -
Definer
处理函数表达式,建立Fun
对象,将实参传给Fun
,并解析得到的字符串,返回Expr
。
-
-
增加
Euler
类来保存exp
指数,实现Factor
接口。 -
修改
Const
为Num
,因为const
为关键字,避免问题。 -
由于递归下降方法的使用,括号嵌套可以直接实现。
具体细节
自定义函数
识别与存储
在输入阶段,首先将Scanner.in
交给Definer
,有Definer
来进行数据的解析。解析时采取正则匹配的方式([fgh])\\(([,uvw]*)\\)=([\\w()+\\-*^]+)
来进行处理,识别出形参与函数的表达式,新建一个Fun
对象来存储,并用hashmap
存储函数,便于通过函数名寻找函数。之后再将Scanner.in
交给Lexer
,进行处理。
函数调用
在Parser
类进行处理的时候,识别到函数调用后,将函数名与函数实参识别出来后,传入Definer.calcFun
,使用Fun.toExpr
对表达式进行字符串替换。
需要注意的是,由于是直接采用的字符串替换,因此若是先将形参y
替换成带x
的实参,再将形参x
替换为别的实参时,前一步的实参也会被替换,因此在函数存储时进行形参的换元,即将xyz
换为不曾出现的uvw
,避免出现问题(替换时也要注意exp
中的x
)。因为调用后的字符串其实是一串表达式,为了不产生其他影响,在最外面加上一层括号。
替换后,再对新字符串进行解析,返回表达式。
//Definer.java public static Expr calcFun(String name, ArrayList<Factor> paras) { Fun fun = funMap.get(name); Lexer lexer = new Lexer(fun.toExpr(paras)); Parser parser = new Parser(lexer); return parser.parseExpr(); } //Fun.java public String toExpr(ArrayList<Factor> realParas) { // (g(x,3*x), x^2, exp(x)) String output = content; for (int i = 0; i < paraList.size(); i++) { String str = "(" + realParas.get(i).toPoly().toString() + ")"; output = output.replaceAll(paraList.get(i), str); } output = "(" + output + ")"; return output; }
指数因子
对指数因子的识别只需要在Parser里面加入一个新的方法即可。真正要注意的,是基本项Mono
的形式改为了a*x^b*exp(Poly)
,因此带来的计算逻辑需要有所修改。
最主要的就是实现指数相等的判断。如果Poly
中使用hashmap
存的基本项,那么可以重写hashcode
与equals
,直接实现hashmap
的相等比较,但是由于我之前使用的是arraylist
,因此采取了另一种方式,重写了基本项的equals
,然后就可以使用contains
函数,从而实现arraylist
的比较。
需要注意的是,由于基本项中exp
内的指数也是Poly
所以判断相等时是两个函数的递归调用。
而在实现了相等后,这一部分其实也变的简单了,仅需要将之前判断系数是否可加减的增加一项调用exp
指数的equals
便可以满足要求。
优化
本次作业由于实现exp指数相等这里耗费了大量的时间,因此优化只做了一小部分。在toString
时对exp
里面的多项式输出进行判断,如果是exp(0)
,则替换为1
。
想到了提取公因式的方法进行化简,但是没有想好具体的实现措施,而且一些情况下,提取后的性能并不一定比提取前好,遂作罢。
对于exp
里面的输出,如果是表达式,则要多加层括号,这个只是通过正则表达式进行识别看里面是否含有"-+*"
,这点使得我的性能变的较差。在第三次作业中进行了修改。
bug分析
课下
-
null & new
如果用a*x^b*exp(Poly)
来存一个基本项,那么对于一个常数来说,Poly
的位置存什么,刚一开始,我存的是null
,但是这样一来,在所有访问Poly
的地方都要进行判断,判断是否为空,但即使这样,也还是不可避免的产生了访问空指针的问题。后来采取了一种,较为浪费空间的方法,即将Poly
处存放一个新的对象,但是里面什么也不存,这样便可避免这一问题,就是空间不太友好。
-
MemoryOverFlow
由于在计算时尝试全部拆开再合并,导致中间过程中MLE
强测
-
TLE
本次作业的强测wa了一个点,是TLE了。在debug的过程中,发现了一个令人哭笑不得的逻辑错误。
正常思维下,判断两个基本项是否可加,仅需要判断指数是否相等即可,但是,如果前面系数非零呢?那就不用判断指数是否相等了,而我却没有实现这一逻辑,导致在运算的过程中,使用了保留了过多的零项,对于这些的计算占用大量时间,从而时间爆掉。
互测
在本次互测过程中,发现了一些同学的bug
-
exp(-x)
:-x
是一个表达式,但是他只算一个基本项,因此有两位同学可能只判断了基本项的个数来决定是否加括号,从而导致错误。 -
exp(OverInt
):在这次的作业中,由于加入了exp
与括号嵌套,从而可以使得指数超过int
的范围。 -
优化的问题:有位同学有如下错误样例,通过合理猜测,这位同学应该是在进行提取公因式时使用了
BigInteger
除法,而未考虑内部指数为0的情况,并且也未考虑全指数为负的情况。
exp(-1)*exp(1) //BigInteger除零错误 (exp(1)*exp(-2))^3 => exp(1)^-3
方法度量
Method Metrics
Complexity metrics | 周四 | 21 3月 2024 19:57:32 CST | ||
---|---|---|---|---|
Method | CogC | ev(G) | iv(G) | v(G) |
Definer.addFun(String) | 2 | 2 | 2 | 2 |
Definer.calcFun(String, ArrayList<Factor>) | 0 | 1 | 1 | 1 |
Definer.init(Scanner) | 1 | 1 | 2 | 2 |
Definer.transform(String) | 0 | 1 | 1 | 1 |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.getNumber() | 2 | 1 | 3 | 3 |
Lexer.next() | 4 | 2 | 4 | 5 |
Lexer.peek() | 0 | 1 | 1 | 1 |
Lexer.pre() | 2 | 1 | 2 | 3 |
MainClass.main(String[]) | 1 | 1 | 2 | 2 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.indexGet() | 1 | 1 | 2 | 2 |
Parser.parseEuler() | 0 | 1 | 1 | 1 |
Parser.parseExpr() | 5 | 1 | 3 | 4 |
Parser.parseFactor() | 8 | 5 | 6 | 6 |
Parser.parseFun() | 1 | 1 | 2 | 2 |
Parser.parseNum() | 4 | 1 | 3 | 4 |
Parser.parseTerm(int) | 1 | 1 | 2 | 2 |
expr.Euler.Euler(Factor) | 0 | 1 | 1 | 1 |
expr.Euler.setIndex(int) | 0 | 1 | 1 | 1 |
expr.Euler.toPoly() | 0 | 1 | 1 | 1 |
expr.Expr.Expr() | 0 | 1 | 1 | 1 |
expr.Expr.addTerm(Term) | 0 | 1 | 1 | 1 |
expr.Expr.setIndex(int) | 0 | 1 | 1 | 1 |
expr.Expr.toPoly() | 2 | 1 | 3 | 3 |
expr.Fun.Fun(String, List<String>) | 0 | 1 | 1 | 1 |
expr.Fun.toExpr(ArrayList<Factor>) | 1 | 1 | 2 | 2 |
expr.Fun.toPoly() | 0 | 1 | 1 | 1 |
expr.Mono.Mono(BigInteger, BigInteger, Poly) | 0 | 1 | 1 | 1 |
expr.Mono.addMono(Mono) | 1 | 1 | 2 | 2 |
expr.Mono.addable(Mono) | 1 | 2 | 1 | 2 |
expr.Mono.clone() | 0 | 1 | 1 | 1 |
expr.Mono.compareTo(Mono) | 0 | 1 | 1 | 1 |
expr.Mono.equals(Object) | 3 | 4 | 1 | 4 |
expr.Mono.isFactor() | 4 | 2 | 4 | 4 |
expr.Mono.multiMono(Mono) | 1 | 1 | 2 | 2 |
expr.Mono.simplify() | 1 | 1 | 1 | 2 |
expr.Mono.toString() | 23 | 2 | 11 | 12 |
expr.Num.Num(BigInteger) | 0 | 1 | 1 | 1 |
expr.Num.setIndex(int) | 0 | 1 | 1 | 1 |
expr.Num.toPoly() | 1 | 1 | 2 | 2 |
expr.Poly.Poly() | 0 | 1 | 1 | 1 |
expr.Poly.addPoly(Poly) | 2 | 1 | 3 | 3 |
expr.Poly.clone() | 1 | 1 | 2 | 2 |
expr.Poly.equZero() | 0 | 1 | 1 | 1 |
expr.Poly.equals(Object) | 5 | 5 | 2 | 5 |
expr.Poly.isFactor() | 2 | 3 | 2 | 3 |
expr.Poly.monoAdd(Mono) | 3 | 3 | 3 | 3 |
expr.Poly.multiPoly(Poly) | 5 | 3 | 4 | 5 |
expr.Poly.sort() | 0 | 1 | 1 | 1 |
expr.Poly.toString() | 4 | 3 | 3 | 5 |
expr.Term.Term(int) | 0 | 1 | 1 | 1 |
expr.Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
expr.Term.toPoly() | 2 | 1 | 3 | 3 |
expr.Var.Var(int) | 0 | 1 | 1 | 1 |
expr.Var.toPoly() | 0 | 1 | 1 | 1 |
具体分析
-
parseFactor()
和lexer.next()
,进行解耦修改,较上次比复杂度有所降低。 -
由于新增加了
exp
指数,导致toString
复杂度大增,但是并没有找到可以解决的办法。
第三次作业
本次作业中需要完成的任务为:读入一系列自定义函数的定义以及一个包含幂函数、指数函数、自定义函数调用、求导算子的表达式,输出恒等变形展开所有括号后的表达式。
总体思路与架构设计
本次作业新增了函数相互调用,求导这两个新功能。
-
由于上次实现的自定义函数解析本身支持互相调用,因此些许修改(
static
立大功) -
新增
Derivation
类来保存要求导的内容,toPoly
调用Mono
的求导并返回Poly
具体细节
求导
Derivation
类中的toPoly
十分简单,先将内部存储的因子变为Poly
,再进行求导。
public Poly toPoly() { Poly poly = factor.toPoly(); return poly.derivation(); }
对于Poly的求导其实就是将内部所有Mono的求导之和相加。
针对于基本项a*x^b*exp(Poly)
,可分为四类:常数,没有x,没有exp,x与exp都有,而最后两种情况会有多项结果,因此求导返回Poly对象。
自定义函数
对于此部分的修改其实也较为简单,只需要对右边的表达式的正则表达式进行修改即可,使其支持fgh
函数名的识别以及参数间隔的,
。
String regex = "([fgh])\\(([,uvw]*)\\)=([\\w()+\\-*^,]+)"
优化
本次的优化主要针对了exp
里面什么时候该加括号,什么时候不加。
首先明确,加括号是在Poly
中的每一项的mono
进行判断,而判断mono
是否加括号,只需要判断exp
里面是否只是一个因子,将这个方法记为poly.isFactor()
,而判断poly.isFactor()
需要进行访问里面的具体内容,因此还得写一个mono.isFactor()
。
//Poly.java private boolean isFactor() { if (monos.size() > 1) { return false; } for (Mono mono : monos) { return mono.isFactor(); } return true; } //Mono.java public boolean isFactor() { if (ceo.compareTo(BigInteger.ZERO) != 0 && ceo.compareTo(BigInteger.ONE) != 0) { return index.compareTo(BigInteger.ZERO) == 0 && exp.equals(new Poly()); } else { return index.compareTo(BigInteger.ZERO) == 0 || exp.equals(new Poly()); } }
因此只需根据isFactor
的返回值,决定是否加括号就行了。
对于提公因式的方法也进行了尝试,但是出现了负优化与bug,随放弃。
方法度量
Method Metrics
Complexity metrics | 周五 | 22 3月 2024 18:00:12 CST | ||
---|---|---|---|---|
Method | CogC | ev(G) | iv(G) | v(G) |
Definer.addFun(String) | 2 | 2 | 2 | 2 |
Definer.calcFun(String, ArrayList<Factor>) | 0 | 1 | 1 | 1 |
Definer.toBeDecided(Scanner) | 1 | 1 | 2 | 2 |
Definer.transform(String) | 0 | 1 | 1 | 1 |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.getNumber() | 2 | 1 | 3 | 3 |
Lexer.next() | 4 | 2 | 4 | 5 |
Lexer.peek() | 0 | 1 | 1 | 1 |
Lexer.pre() | 2 | 1 | 2 | 3 |
MainClass.main(String[]) | 1 | 1 | 2 | 2 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.indexGet() | 1 | 1 | 2 | 2 |
Parser.parseDerivative() | 0 | 1 | 1 | 1 |
Parser.parseEuler() | 0 | 1 | 1 | 1 |
Parser.parseExpr() | 5 | 1 | 3 | 4 |
Parser.parseFactor() | 9 | 6 | 7 | 7 |
Parser.parseFun() | 1 | 1 | 2 | 2 |
Parser.parseNum() | 4 | 1 | 3 | 4 |
Parser.parseTerm(int) | 1 | 1 | 2 | 2 |
expr.Derivative.Derivative(Factor) | 0 | 1 | 1 | 1 |
expr.Derivative.toPoly() | 0 | 1 | 1 | 1 |
expr.Euler.Euler(Factor) | 0 | 1 | 1 | 1 |
expr.Euler.setIndex(int) | 0 | 1 | 1 | 1 |
expr.Euler.toPoly() | 0 | 1 | 1 | 1 |
expr.Expr.Expr() | 0 | 1 | 1 | 1 |
expr.Expr.addTerm(Term) | 0 | 1 | 1 | 1 |
expr.Expr.setIndex(int) | 0 | 1 | 1 | 1 |
expr.Expr.toPoly() | 2 | 1 | 3 | 3 |
expr.Fun.Fun(String, List<String>) | 0 | 1 | 1 | 1 |
expr.Fun.toExpr(ArrayList<Factor>) | 1 | 1 | 2 | 2 |
expr.Fun.toPoly() | 0 | 1 | 1 | 1 |
expr.Mono.Mono(BigInteger, BigInteger, Poly) | 0 | 1 | 1 | 1 |
expr.Mono.addMono(Mono) | 0 | 1 | 1 | 1 |
expr.Mono.addable(Mono) | 1 | 2 | 1 | 2 |
expr.Mono.clone() | 0 | 1 | 1 | 1 |
expr.Mono.compareTo(Mono) | 0 | 1 | 1 | 1 |
expr.Mono.derivation() | 5 | 1 | 5 | 5 |
expr.Mono.equals(Object) | 3 | 4 | 1 | 4 |
expr.Mono.isFactor() | 5 | 2 | 5 | 5 |
expr.Mono.multiMono(Mono) | 1 | 1 | 2 | 2 |
expr.Mono.simplify() | 1 | 1 | 1 | 2 |
expr.Mono.toString() | 23 | 2 | 11 | 12 |
expr.Num.Num(BigInteger) | 0 | 1 | 1 | 1 |
expr.Num.setIndex(int) | 0 | 1 | 1 | 1 |
expr.Num.toPoly() | 1 | 1 | 2 | 2 |
expr.Poly.Poly() | 0 | 1 | 1 | 1 |
expr.Poly.addPoly(Poly) | 2 | 1 | 3 | 3 |
expr.Poly.clone() | 1 | 1 | 2 | 2 |
expr.Poly.derivation() | 1 | 1 | 2 | 2 |
expr.Poly.equZero() | 0 | 1 | 1 | 1 |
expr.Poly.equals(Object) | 5 | 5 | 2 | 5 |
expr.Poly.isFactor() | 2 | 3 | 2 | 3 |
expr.Poly.monoAdd(Mono) | 3 | 3 | 3 | 3 |
expr.Poly.multiPoly(Poly) | 5 | 3 | 4 | 5 |
expr.Poly.sort() | 0 | 1 | 1 | 1 |
expr.Poly.toString() | 4 | 3 | 3 | 5 |
expr.Term.Term(int) | 0 | 1 | 1 | 1 |
expr.Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
expr.Term.toPoly() | 2 | 1 | 3 | 3 |
expr.Var.Var(int) | 0 | 1 | 1 | 1 |
expr.Var.toPoly() | 0 | 1 | 1 | 1 |
分析
还是toString
爆了!!!!,设想将toString
分开成为几个函数,分别生成对应代码,但是感觉拆开后这几个的方法仍然有相当高的耦合度,也不符合oo的要求。
同时parseFactor
,也有些大,因为其调用了多个不同因子的处理函数,导致条件判断过多。
Bug分析与思考
最后一次的bug,主要产生在优化方面,因为别的内容基本没有修改,而求导方法的实现所需要考虑的情况数也不多。
首先是我的优化bug,在进行加括号判断的时候,最外层的多项式也会被加上括号,但是由于这是基于我的架构的最简单判断是否加括号的方法,因此对于此bug的修改只是将最外层的括号删去。
优化中其实有一部分是基础性优化,是很轻松就可以拿到的分数,比如合并同类项,比如正号提前,将这些做到,便可以拿到80~90%的性能,因为正确性还是首要的,因此可以进行化简而得到很短的表达式的数据点很少。而剩下一两个则是专门为优化设置的数据点,如果你没有想到最简的那种优化,大概率还是性能得不到分。
而在评测中,性能占用15%,而正确性占85%,一个测试点的正确分就抵得上近6个性能分。进行一个优化,而导致测试出现bug,是一件很亏的事情,况且优化在某些情况下可能还是负优化,所以除非是特别笃定这个优化一定没有错误,并且绝大多数情况下都是正优化,那么可以提交,反之还是要慎重。
当然,这样并不是说不去优化,而是说在优化后要多进行测试,保证不会出现问题,即使这一部分要用比优化更长的时间。
总结
迭代式开发
单次作业
在每一次单独作业进行开发的时候,也要注意迭代的思想。比如在第二次的作业中,在构建前,先想清楚要分为哪几步,比如自定义函数表达式的存储,自定义函数的调用,exp指数基本项的实现,exp的计算修改。然后再迭代进行,每次进行完一步之后,对上一步的功能进行检查与验证,无误后在进行下一步。在这一过程中,对于迭代目标也有可能进行一定的修改,但是这本身也是一种正常的迭代。相比较第一次从头到尾的开发,对于代码的整体框架的有了更清楚的把握。
作业之间
前一次在设计的时候,尽量考虑周全,考虑可拓展性,尤其要注意一些边界条件的设计要求,比如说,第一次要求不会出现嵌套括号,便要考虑后续是否会需要支持多层括号。又比如题目要求只有未知变量x,会不会出现yz等其他变量。这些都应该在设计时都考虑进来,如果加了这些要求,要怎样才能在原来的基础上进行少量的增添就能解决这一问题,或者直接一次设计就支持后面的部分功能。
类的架构
本次作业中的类的架构其实是有一些是问题的,一部分的类有些冗余,另一部分类有些过度耦合。
Mono与Term
mono
类实现了基本项的存储,但是我却把他与term
分开,但是mono
都叫做基本项
了,那其实他和term
类的逻辑关系是很密切的。但是他们两个的内容还是不太一样的,采用继承的方式也不是很能描述这一关系。或许也应该使用接口,将两个类都继承一个新的NewTerm
的接口,从而最终可以不去新建Poly
类,而是使用Expr类解决这一问题。
Mono与Poly
在我的实现中,Poly
内部会有arraylist
来存mono
,而mono
里面的exp
又会去保存一个Poly
,层层嵌套,导致函数的计算写起来也得层层调用,debug时候更是难上加难。但是仔细想来,由于exp
里面深度的不确定性,也只能写这样的一个循环。并不能将这两个方法进行解耦或者是合并。
exp没留指数
在Poly
的设计中,并没有保留指数这一属性,导致优化时提因子后,并不能有一个合理的位置来存储,从而导致公因数来回传参,最终使得这一架构在优化过程中变的十分棘手,没有找到很好的既可以进行优化,又能保证正确性的方法。
寻找bug
首先通过自动数据生成器构造大量数据,进行测试,若有问题,则将该数据点进行一点点的拆解,将不会造成bug的部分删去,最终得到诱导bug的数据。
而若是数据生成器测不出问题,则通过自己在写代码时出现的问题,进行进一步的测试,同时构造一些特殊情况,或者进行压力测试。
心得体会
本单元的作业总体经历了三个阶段,迷茫无助,痛苦煎熬,如释重负。在好久没有接触oo并且上来就是完全没有学过的递归下降,与复杂的形式化定义,对于如此庞然大物的处理,有些不知所措,所幸有老师、有助教、有往届学长留下的博客,得以让我成功度过第一周。而第二周的exp是全新的内容,思考其相等的判断,以及Poly
与Mono
的反复调用,令我cpu过载,每天从早到晚,充斥着oo,甚至梦里也没有放过我,不过还好,挺过来了。感谢前两周的辛勤努力与对可拓展性的思考与实现,使得我在第三次的作业中,较为轻松。
至于未来的几个单元,且行且看,无论有多么痛苦与无助,相信我一定能坚持下来,再次达到如释重负。
未来方向
可以考虑展出一些优秀的架构的与参考,仅凭大家课堂展示的分享,可能并不能get到其中架构设计时的精华所在。要在课下将自己的架构与优化架构进行反复的比较与思考,才能向更好的架构前进。