OO第一单元总结
面向对象设计与构造第一单元博客
第一次作业
考虑到以后作业的迭代,虽然本次作业用正则表达式也能完成,还是采用递归下降的方法,把括号嵌套也做了。
类图展示
程序逻辑
先把输入的表达式tokenize
然后遍历token的同时进行parse建立表达式树
遍历前先在表达式的左右两边均加上括号方便统一进行操作
注意在parse操作时,每读取一个token后均会将其删去
重要类/接口分析
Parser
ParseE:
对于每一个expression,他的组成为一系列的term相加(term是有正有负的),因此在parseE的时候只需要返回一个含有所有Term的ArrayList即可得到一个表达式
对于表达式而言 该表达式在未达到最后一个右括号时尚未结束,因此用while循环进行表达式内项的遍历
每当遇到加减号时,对相应的term的符号pos进行改变,在term前加入*方便统一操作,然后调用ParseT对里面的term进行解析,并将解析到的term加入ArrayList中。重复进行该操作。
遇到右括号并且只剩下右括号时结束parse expression的过程,返回该Expression
ParseT:
对于每一个term,它的组成为一系列factor相乘,因此在parseT的时候只需要返回一个含有所有Factor的ArrayList即可得到一个项
当term遇到乘号时,接下来的应该是一个Factor(预处理使得第一个factor前也带乘号),因此用while循环调用ParseF解析factor,循环重复以上操作。
ParseF:
判断1:
若是数字、自变量,可以直接对该token进行建立叶结点的操作,将其值传入对应的类;
若是左括号,说明是(E)的操作,因此对内部的表达式进行解析,在遇到其对应的右括号时会返回括号内的子表达式,从而建立表达式结点作为factor。
判断2:
若是**,说明是一个幂函数,此时前面已经生成了一个factor,也就是该幂函数的base,将其传入幂函数的类,其后的数字为其exp,也传入幂函数的类
Factor类
Factor类只有一个方法,即化简计算,是许多类的父类,该方法也会被重写
Term类
Term类有两个属性,pos代表该项为正/负,另外一个Factor组成的ArrayList包含了所有组成该Term的Factor
Term类的化简计算方式十分简单,只需要把每个化简计算后的Factor结果用乘法的分配律相乘则可以得出结果。
Expression类
Expression类包含所有组成他的Term,其化简计算方式也很简单,是把每个化简计算后的term用加法把对应相同指数的部分系数相加即可。
Power类
Power类有base(底数)以及exp(指数),化简计算时可以进行特殊判断,若exp为0,则结果为1,若base为0,结果为0,反之则可以将其转化成exp个base组成的term来进行化简计算。
各类方法复杂度
Method | CogC | ev(G) | iv(G) | v(G) |
Constant.Constant(BigInteger) | 0 | 1 | 1 | 1 |
Constant.getValue() | 0 | 1 | 1 | 1 |
Constant.simplify() | 0 | 1 | 1 | 1 |
Constant.toString() | 0 | 1 | 1 | 1 |
Expression.Expression(ArrayList |
0 | 1 | 1 | 1 |
Expression.getTerms() | 0 | 1 | 1 | 1 |
Expression.simplify() | 1 | 1 | 2 | 2 |
Expression.toString() | 1 | 1 | 2 | 2 |
Factor.simplify() | 0 | 1 | 1 | 1 |
ID.ID(String) | 0 | 1 | 1 | 1 |
ID.simplify() | 0 | 1 | 1 | 1 |
ID.toString() | 0 | 1 | 1 | 1 |
Main.lexer(String) | 6 | 1 | 4 | 4 |
Main.main(String[]) | 3 | 1 | 3 | 3 |
Main.shorten(Map) | 23 | 3 | 11 | 12 |
Parser.Parser(ArrayList |
0 | 1 | 1 | 1 |
Parser.parseE() | 10 | 3 | 5 | 6 |
Parser.parseF() | 8 | 6 | 6 | 6 |
Parser.parseT(boolean) | 3 | 1 | 4 | 4 |
Power.Power(Factor, BigInteger) | 0 | 1 | 1 | 1 |
Power.simplify() | 10 | 6 | 6 | 7 |
Term.Term(boolean, ArrayList |
0 | 1 | 1 | 1 |
Term.mult(HashMap |
3 | 1 | 3 | 3 |
Term.simplify() | 7 | 2 | 5 | 5 |
Term.toString() | 1 | 1 | 2 | 2 |
Token.Token(Type, String) | 0 | 1 | 1 | 1 |
Token.getName() | 0 | 1 | 1 | 1 |
Token.getType() | 0 | 1 | 1 | 1 |
compare(Entry |
0 | n/a | n/a | n/a |
合并同类项的方法复杂度较高,考虑到对于表达式相乘的情况需要用到for循环嵌套。
代码规模分析
Constant.java | 26 | 21 | 0.8076923076923077 | 0 | 0.0 | 5 | 0.19230769230769232 |
Expression.java | 35 | 30 | 0.8571428571428571 | 0 | 0.0 | 5 | 0.14285714285714285 |
Factor.java | 10 | 8 | 0.8 | 1 | 0.1 | 1 | 0.1 |
ID.java | 22 | 18 | 0.8181818181818182 | 0 | 0.0 | 4 | 0.18181818181818182 |
Main.java | 107 | 95 | 0.8878504672897196 | 6 | 0.056074766355140186 | 6 | 0.056074766355140186 |
Parser.java | 80 | 75 | 0.9375 | 0 | 0.0 | 5 | 0.0625 |
Power.java | 44 | 41 | 0.9318181818181818 | 0 | 0.0 | 3 | 0.06818181818181818 |
Term.java | 58 | 52 | 0.896551724137931 | 1 | 0.017241379310344827 | 5 | 0.08620689655172414 |
Token.java | 17 | 14 | 0.8235294117647058 | 0 | 0.0 | 3 | 0.17647058823529413 |
Type.java | 3 | 3 | 1.0 | 0 | 0.0 | 0 | 0.0 |
代码量较大,但比较均匀的分布在不同的类中
优缺点分析
优点分析
采用递归下降的方式,为后续的迭代留下了较好的架构,可拓展性强
问题分析
只有一个子结点的树较多,没有进行特殊处理,导致运行花费时间较长,占用空间较多。
优化总结
合并同类项
\(x**2*x**6\)化简成了\(x**8\)
\(x**2+x**2\)化简成了\(2*x**2\)
正项输出提前
通过(系数)降序输出不同的项,保证若有正项,第一项一定为正
\(-1+x\)化简成了\(x-1\)
平方输出优化
\(x**2\)化简成了\(x*x\)
指数输出优化
\(x**1\)化简成了\(x\)
互测总结
互测时采用狂轰滥炸手段,首先hack到了一位格式错误的兄弟。
互测的时候也发现一份代码只要到了x^16 就会输出错误
认真看了一下这位同学的代码,发现TA的输出方式有一定的问题。
for (int key : expr.getPoly().keySet()) {
BigInteger coefficient = expr.getPoly().get(key);
if (coefficient.equals(BigInteger.ZERO)) {
continue;
}
//不重要的代码
}
这位同学采用的是遍历keySet的方式打印HashMap的内容,但是这种方法有一个问题,set内的元素实际上是无序的,但出于某种不知道的原因,他其实会按照key值递增输出。
那么在他的代码里面,指数为0的(常数)会先输出,若为正数,则不需要输出"+",但实际上HashMap的初始容量是16,key=16的元素在输出时会跑到前面,导致正常数在后面输出的时候没有"+"。
第二次作业
本次作业比起上一次,增加了自定义函数、sum函数、三角函数以及括号嵌套。括号嵌套在第一次作业的时候已经完成了。
类图展示
(由于第三次作业与第二次作业没有区别,在此放上第二次和第三次作业递交程序的类图)
自定义函数&sum函数
尝试了使用正则表达式来进行替换,但发现正则表达式问题多多,贪婪匹配也不对,非贪婪匹配也不对,考虑到接下来的第三次作业的函数迭代,放弃了正则表达式的方法。
为了最大程度复用代码,采用了在预解析的时候就把相应的函数转换成表达式的方法。由于已经tokenize了输入的字符串,因此只需要把函数式中的x,y,z等变量换成(调用时的表达式)即可将函数转换成表达式,从而可以直接使用原先写的表达式解析的方法。
在识别需要套入x,y,z变量的表达式分别是什么的时候,采用了最原始的方法:栈。即在开始识别的时候设置一个bracketCounter,识别到左括号+1,右括号-1,在该计数器达到0时并且遇到逗号时说明已经识别完毕(最后一个变量不需要逗号)。用这个方式可以处理嵌套函数、三角函数等内容。
三角函数
由于第一次作业最终的变量只有x,用简单的指数和底数构成HashMap的方式进行合并即可。
第二次作业明显不能通过简单的\(HashMap<BigInteger,BigInteger>\)进行合并同类项,因此采用了\(HashMap<HashSet<Power>,BigInteger>\),key是除了常数以外的所有因子,value是常数。考虑到乘法因子顺序不同但是内容相同的项是可以合并的,Term中的ArrayList就很难达到合并的目的,因此采用HashSet解决该问题。
为了让相同的Expression/Term/Factor可以被识别成相同的对象,需要重写HashCode以及equals的方法,可以用IDEA直接生成。
由于一开始没有想清楚就开始写,连续两次都半途而废,最后一次把思路都写到纸上,把它捋清楚了之后再开始写代码,发现可以在原来的代码上迭代开发,完成了合并的操作。
合并同类项采用的是递归的方法,对每一个Expression,Term,Factor均进行合并操作。
对Expression而言,只需要把其包含的Term生成的HashMap进行merge操作即可。
对Term而言,需要对包含的Factor生成的HashMap进行相乘操作,其HashSet的相乘(也就是非常数的因子相乘)操作可以单独写一个函数,注意合并base相同的power。
优化总结
在计算结果的时候单独写了一个方法进行优化
平方和优化
考虑到\(sin^2(x)+cos^2(x) = 1\),在优化的过程中对所有含有\(sin^2(base)\)的HashSet进行了操作。对于每一个base,将其所有含该\(sin^2\)的项提出后形成一个新的HashMap,遍历所有HashSet查找含有\(cos^2(base)\)的HashSet,并且把\(cos^2(base)\)替换成\(1-sin^2(base)\),提出后又形成一个HashMap并与前一个HashMap合并。但这样做其实问题很大,因为全部转换成\(cos^2(base)\),其结果不一定比原来短。
sin(0)优化
考虑到\(sin(0)=0\),因此在计算的时候,遇到sin(base)则对其base进行计算,若base为零则把整个包含该sin(base)的HashSet去除(乘零为零)。但这样属于重复递归调用计算的方法(化简外部表达式时调用一次,计算本身结果后打印的时候又调用一次),效率较低。
各类方法复杂度
Method | CogC | ev(G) | iv(G) | v(G) |
CalRes.baseMerge(HashSet |
0 | 1 | 1 | 1 |
CalRes.calExpression(Expression) | 1 | 1 | 2 | 2 |
CalRes.calFactor(Factor) | 16 | 1 | 10 | 11 |
CalRes.calTerm(Term) | 4 | 1 | 4 | 4 |
CalRes.cosNeg(Power, HashMap |
20 | 7 | 8 | 8 |
CalRes.mult(HashMap |
3 | 1 | 3 | 3 |
CalRes.simNeg(HashMap |
30 | 1 | 9 | 9 |
CalRes.simRes(HashMap |
24 | 1 | 11 | 11 |
CalRes.triSim(HashMap |
17 | 1 | 6 | 7 |
Constant.Constant(BigInteger) | 0 | 1 | 1 | 1 |
Constant.equals(Object) | 3 | 3 | 2 | 4 |
Constant.getValue() | 0 | 1 | 1 | 1 |
Constant.hashCode() | 0 | 1 | 1 | 1 |
Constant.toString() | 0 | 1 | 1 | 1 |
Cos.Cos(Factor) | 0 | 1 | 1 | 1 |
Cos.equals(Object) | 2 | 3 | 1 | 3 |
Cos.getBase() | 0 | 1 | 1 | 1 |
Cos.hashCode() | 0 | 1 | 1 | 1 |
Cos.setBase(Factor) | 0 | 1 | 1 | 1 |
Cos.simplified() | 0 | 1 | 1 | 1 |
Expression.Expression(ArrayList |
0 | 1 | 1 | 1 |
Expression.equals(Object) | 3 | 3 | 2 | 4 |
Expression.getTerms() | 0 | 1 | 1 | 1 |
Expression.hashCode() | 0 | 1 | 1 | 1 |
Expression.simplified() | 4 | 2 | 3 | 3 |
Expression.toString() | 1 | 1 | 2 | 2 |
Factor.getBase() | 0 | 1 | 1 | 1 |
Factor.simplified() | 0 | 1 | 1 | 1 |
ID.ID(String) | 0 | 1 | 1 | 1 |
ID.equals(Object) | 2 | 3 | 1 | 3 |
ID.hashCode() | 0 | 1 | 1 | 1 |
ID.toString() | 0 | 1 | 1 | 1 |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.addBr(ArrayList |
1 | 1 | 2 | 2 |
Lexer.brackCount(Type, int) | 3 | 3 | 1 | 3 |
Lexer.extractVar(ArrayList |
7 | 3 | 4 | 5 |
Lexer.functionLexer() | 0 | 1 | 1 | 1 |
Lexer.getVar(ArrayList |
10 | 3 | 5 | 6 |
Lexer.isNum(ArrayList |
1 | 1 | 2 | 2 |
Lexer.sentenceLexer(HashMap |
10 | 4 | 7 | 7 |
Lexer.sumFun(BigInteger, BigInteger, ArrayList |
7 | 1 | 4 | 4 |
Lexer.switchFunc(ArrayList |
9 | 1 | 7 | 7 |
Lexer.transFunc(ArrayList |
21 | 7 | 9 | 11 |
Lexer.transSum(ArrayList |
1 | 1 | 2 | 2 |
Main.main(String[]) | 1 | 1 | 2 | 2 |
Parser.Parser(ArrayList |
0 | 1 | 1 | 1 |
Parser.parseE() | 11 | 3 | 6 | 7 |
Parser.parseF() | 12 | 8 | 9 | 9 |
Parser.parseT(boolean) | 3 | 1 | 4 | 4 |
Power.Power(Factor, BigInteger) | 0 | 1 | 1 | 1 |
Power.equals(Object) | 4 | 3 | 3 | 5 |
Power.getBase() | 0 | 1 | 1 | 1 |
Power.getExp() | 0 | 1 | 1 | 1 |
Power.hashCode() | 0 | 1 | 1 | 1 |
Power.simplified() | 4 | 4 | 4 | 5 |
Print.Print(HashMap |
0 | 1 | 1 | 1 |
Print.identifyFac(HashMap |
14 | 6 | 5 | 8 |
Print.printCoe(BigInteger, boolean, boolean) | 8 | 1 | 5 | 7 |
Print.printExpr(boolean) | 13 | 4 | 8 | 11 |
Print.printPow(Power, boolean) | 17 | 2 | 15 | 20 |
Print.printPows(HashSet |
35 | 8 | 9 | 15 |
Sin.Sin(Factor) | 0 | 1 | 1 | 1 |
Sin.equals(Object) | 2 | 3 | 1 | 3 |
Sin.getBase() | 0 | 1 | 1 | 1 |
Sin.hashCode() | 0 | 1 | 1 | 1 |
Sin.simplified() | 0 | 1 | 1 | 1 |
Term.Term(boolean, ArrayList |
0 | 1 | 1 | 1 |
Term.equals(Object) | 4 | 3 | 3 | 5 |
Term.getFactors() | 0 | 1 | 1 | 1 |
Term.hashCode() | 0 | 1 | 1 | 1 |
Term.isPos() | 0 | 1 | 1 | 1 |
Term.mult(HashMap |
3 | 1 | 3 | 3 |
Term.simplified() | 6 | 3 | 5 | 5 |
Term.toString() | 1 | 1 | 2 | 2 |
Token.Token(Type, String) | 0 | 1 | 1 | 1 |
Token.getName() | 0 | 1 | 1 | 1 |
Token.getType() | 0 | 1 | 1 | 1 |
代码量分析
CalRes.java | 300 | 288 | 0.96 | 2 | 0.006666666666666667 | 10 | 0.03333333333333333 |
Constant.java | 36 | 30 | 0.8333333333333334 | 0 | 0.0 | 6 | 0.16666666666666666 |
Cos.java | 41 | 34 | 0.8292682926829268 | 0 | 0.0 | 7 | 0.17073170731707318 |
Expression.java | 59 | 52 | 0.8813559322033898 | 0 | 0.0 | 7 | 0.11864406779661017 |
Factor.java | 7 | 6 | 0.8571428571428571 | 0 | 0.0 | 1 | 0.14285714285714285 |
ID.java | 31 | 26 | 0.8387096774193549 | 0 | 0.0 | 5 | 0.16129032258064516 |
Lexer.java | 283 | 258 | 0.911660777385159 | 12 | 0.04240282685512368 | 13 | 0.045936395759717315 |
Main.java | 45 | 28 | 0.6222222222222222 | 14 | 0.3111111111111111 | 3 | 0.06666666666666667 |
Parser.java | 95 | 88 | 0.9263157894736842 | 2 | 0.021052631578947368 | 5 | 0.05263157894736842 |
Power.java | 50 | 43 | 0.86 | 0 | 0.0 | 7 | 0.14 |
Print.java | 195 | 184 | 0.9435897435897436 | 4 | 0.020512820512820513 | 7 | 0.035897435897435895 |
Sin.java | 37 | 31 | 0.8378378378378378 | 0 | 0.0 | 6 | 0.16216216216216217 |
Term.java | 80 | 70 | 0.875 | 1 | 0.0125 | 9 | 0.1125 |
Token.java | 17 | 14 | 0.8235294117647058 | 0 | 0.0 | 3 | 0.17647058823529413 |
Type.java | 4 | 4 | 1.0 | 0 | 0.0 | 0 | 0.0 |
比起第一次作业代码量增加了很多,主要是为了完成各种优化以及合并同类项,在计算的类以及函数替换类中写了大量的方法进行处理。
强测总结
非常无语的是强测wa了一个点,还是由于无脑优化产生的!!
在交之前突然想到\(cos(0)=1\),但是又懒得像优化\(sin(0)\)那样单独写一个方法,最后居然一时脑抽决定把最后输出的字符串ans进行暴力替换(......),修改时还思考了一下觉得没有毛病(......)。最终造成输出了\(1**3\)这样的非法格式(常数不能带指数)。
这件事情告诉我们:
1.优化就好好优化,不要优化一半不优化一半,那还不如不要优化。
2.字符串替换需谨慎!!!
3.认真看BNF表述,按照题目要求来进行构造与检验
互测总结
这次互测没有被刀(可能别人也没认真看BNF)。测出来有人优化的时候,把sin内的负号无脑提出来了,忘记了sin可能还带偶指数。还有一个hack到的bug是炸出来的,没有发现具体原因。
第三次作业
由于在第二次作业中已经处理了大多数第三次作业的情况,本次作业只需要再处理一下迭代函数的情况。
优化总结
平方和优化
上一次优化的时候无脑把\(sin^2(base)\)替换了,但是效果并不好。这一次作业重新写了一下平方的优化。把sin^2提出来后形成新的HashMap再合并的形式与之前的做法是一样的,但这次分别把\(sin^2(base)\)与\(cos^2(base)\)进行替换,即分别为:
\(asin^2+bcos^2+c\) -----------> \((b-a)cos^2+(a+c)\)
\(asin^2+bcos^2+c\) -----------> \((a-b)sin^2+(b+c)\)
然后通过判断"b-a+a+c"与"a-b+b+c"两个字符串长度得出更优的是哪一种,再将计算后的结果放回原来的HashMap。
cos(0)优化
这次吸取了教训,不敢再暴力字符串替换了,像第二次作业那样,写了一个函数遍历,若cos(base)中的base计算的结果为0,则把该Power化成1。
三角内符号优化
\(cos(-x)=cos(x)\):
由于在计算结果的时候采用的是递归的方法,我的程序在收到cos(base)的时候是并不清楚base中是什么东西的,因此又需要调用一次计算的方法将其中的base计算出来,如果是带负号的只有一项的表达式,便可以把负号去除。
但当时一直不明白如何在替换后合并同类项,因为合并同类项操作已经完成了,我只能把\(cos(x)+cos(-x)\)变成\(cos(x)+cos(x)\),优化效率远远不够。强测结束后才发现,其实我只需要把得到的字符串重新输入到程序里再进行一次计算就可以了,由于已经简化了不少,计算强度不会很高。
\(sin(-x)=-sin(x)\):
第二次互测的时候发现了不少在这里翻车的,所以化简的时候特别的小心,若该三角函数的指数为偶数,则直接把负号去除,若为奇数,则把负号提至常数系数处。
由于程序和第二次结构一样,在此不放一样的类图和方法复杂度了
强测总结
强测又G了......
wa的两个点都是因为三角函数平方和优化的时候出了错。互测被hack的也是这个bug。
我采用的三角优化方式是把\(sin^2\)提出来,剩下的部分组成一个HashMap,但这次在重写上一次方法的时候忘记了其剩下的部分可能为常数(上一次是想到了的),导致提出的HashMap可能出现键值为空的情况,无法输出该常数。
这个完完全全是可以由充分测试de出来的,简单的\(sin^2(x)+cos^2(x)\)在我这里的结果都是0而不是1(.....),在此向因为hack到了这个而怀疑自己进了C房的同房同学道歉(
得出的结论就是:有事没事,多做测试,这样的错误完全可以通过手造几组最基本的数据发现!
互测总结
互测被刀了两下,hack到的bug主要是:
1.sum没有考虑下界比上界高结果为0的情况
2.sum的上下界用int而不是BigInteger,出现爆int但是cost不爆的情况
架构迭代
在第一次作业的时候考虑到后续出现多种因子,括号嵌套的情况,因此采用了递归下降的方法,第二次作业尝试重构无果后在原作业的基础上更改了计算因子的方法从而达到合并同类项的效果。第三次作业相较第二次作业,得益于较好的架构,只增加了嵌套函数的替换token方法。
心得体会
本单元的作业在第二、三次强测均出现了bug。但这些bug完全是因为没有付出更多的时间去做测试,去认真看bnf说明造成的。这些由于优化产生的bug是完完全全可以避免的,例如最后一单元的\(sin^2+cos^2=0\),只需要在写完优化方式后认真的手造几组最基础的数据就可以完成,但由于过于懈怠,依赖评测机和同学对拍造的复杂数据来测试而忽略了最基本的情况。
同时在完成作业的时候出现的很多问题都是由于寒假的时候没有预习造成的,寒假因为玩的太疯完全没有学习,没有做pre的任务,导致开学后很多关于Java的基本语法,面向对象的基本概念都不太清楚,花费大量时间先把Pre完成后才开始完成正式作业,过程很坎坷。