301
社区成员
发帖
与我相关
我的任务
分享本单元作业的主题是多项式展开与化简,既要将表达式进行展开,以去除不必要的括号,也要将展开的表达式进行合并,得到更精炼的输出以及更高的性能分。
本次作业需要完成的任务为:读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 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
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里面的输出,如果是表达式,则要多加层括号,这个只是通过正则表达式进行识别看里面是否含有"-+*",这点使得我的性能变的较差。在第三次作业中进行了修改。
课下
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的修改只是将最外层的括号删去。
优化中其实有一部分是基础性优化,是很轻松就可以拿到的分数,比如合并同类项,比如正号提前,将这些做到,便可以拿到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的数据。
而若是数据生成器测不出问题,则通过自己在写代码时出现的问题,进行进一步的测试,同时构造一些特殊情况,或者进行压力测试。
本单元的作业总体经历了三个阶段,迷茫无助,痛苦煎熬,如释重负。在好久没有接触oo并且上来就是完全没有学过的递归下降,与复杂的形式化定义,对于如此庞然大物的处理,有些不知所措,所幸有老师、有助教、有往届学长留下的博客,得以让我成功度过第一周。而第二周的exp是全新的内容,思考其相等的判断,以及Poly与Mono的反复调用,令我cpu过载,每天从早到晚,充斥着oo,甚至梦里也没有放过我,不过还好,挺过来了。感谢前两周的辛勤努力与对可拓展性的思考与实现,使得我在第三次的作业中,较为轻松。
至于未来的几个单元,且行且看,无论有多么痛苦与无助,相信我一定能坚持下来,再次达到如释重负。
可以考虑展出一些优秀的架构的与参考,仅凭大家课堂展示的分享,可能并不能get到其中架构设计时的精华所在。要在课下将自己的架构与优化架构进行反复的比较与思考,才能向更好的架构前进。