面向对象第一单元总结

面向对象第一单元总结

fishlife

写在前面

​ 除去小学时写过的”文明小博客“之外,这是我第一次正儿八经地写一篇博文,希望能讲清楚事情的同时写出自己的风格。

​ 第一单元的面向对象作业,总体而言完成的还算顺利,因为有前几届学长学姐的博客参考,虽然我们的具体内容有了较大差别,但还是能够通过往届的博客大致预测出下一次的作业风向,也就能更好地进行架构设计。但这其中也暴露出来一些问题需要重视。

本单元关键词:初见与”叛逆“

​ 初见很好理解,第一次接触面向对象,对java语言的了解不多,虽然认真做完了pre,完成作业时还是会需要通过搜索引擎查找一些没用过的java代码操作,以扩充自己的java知识。

​ 至于”叛逆“一词,是这么解释的:这次作业里,我的解决方法基本上就是指导书反着来,指导书不推荐的事情我全做了一遍,构成了自己的第一单元作业。这其中有很多的原因,但我后来发现,把这些不被看好的事物组合在一起,可能会构成另一条通向终点的道路。

分次作业分析

第一次作业

作业分析

​ 第一次作业只需要对多项式进行化简,相对而言架构设计比较简单。我的思路是采用TreeMap存储幂函数,Key为指数,Value为幂函数类PowerFun,常数则视为次数为0的幂函数,因为这样可以将常数和幂函数统一,可以通过Key来判断同类项,方便化简。计算基本因子为\(ax^b\)。但在我思考计算化简的思路时却陷入了困境,我原本想采用递归下降的思路,所以我翻查了资料打算学会它,但是在当时的我眼里,递归下降更适合作为一个判定方法而非计算方法,打个比方就是这方法做判断题好用但是做填空题不好使。终于,我在跟他鏖战两天后决定采取一个折衷的方法,即先用不推荐的后缀表达式做法,但是为后续的递归下降留好可以复用的类或模块。所以在第一次作业里我使用的还是数据结构学到的老方法,但是向里面混入了一些面向对象的思想,对表达式组成成分进行分析封装,最终诞生了这么一个“缝合怪”。

​ 为了简化计算,我设计了预处理器PreProgressor对输入串作如下处理:

  • 去除空白符
  • 将重复符号等价替换,如“++”替换为“+”,“-++”替换为“-”。
  • 将乘方符号等价替换,“**”替换为“^“
  • 格式化,战术补0,在所有单独带有符号的数后面补0,如“+1”改为“0+1”,上面两步操作后,串内仅可能存在[*^][+-]和([+-]两种符号相连的情况,对这两种情况特别处理。如“2*+3”改为“2*(0+3)”,“2^+3”改为“2^(0+3)”。

​ 容易发现,上面一系列操作的最重要目的是防止污染符号栈,当连续两个符号进栈后可能导致最后符号栈与计算因子栈无法匹配而出错。

​ 这次的化简只涉及多项式,所以基本上直接做在了计算部分,主要为合并同类项。在输出部分也通过专门编写输出方法以及改写PowerFun的toString方法来进行输出简化,包括省略系数1,负项推迟输出,x**2简化为x*x等。

​ 第一次作业的类图如下

tips: 由于staruml以及其他规则的限制,此处部分容器类型的格式与一般格式不同,但均可轻易地辨识出原格式,对阅读影响不大, 下同

​ 类图优点是每个类各司其职,相互耦合性较小,且每个模块均可单独拿出来作为别的程序的组件使用。

​ 缺点则是这种采用工具类的思想会使得类数量和类内部方法的数量之间难以达到一个平衡。二者之间总有一个会在数量产生膨胀从而导致复杂度难以降低。并且每个类显得过于分散,导致整体架构相对零散。

​ 每个类的文字解释如下:

解释
Main 主类,包含主方法以及无法归到某个工具类的方法
Factor 因子类
Oper 运算符类,用于封装处理运算符
PowerFun 幂函数(带系数)类,用于处理常数及幂函数
Lexer 分析器,用于从式子中按顺序获取运算符或幂函数
CalCulator 计算器,用于计算并化简式子
PreProgressor 预处理器,用于对输入串进行预处理,简化后续计算操作
TreeCreator 树生成器,更严谨讲应该是后缀表达式生成器,用于生成后缀表达式。写的时候一直想着递归下降重构,而重构后实际上生成的也是棵树,故采用此名,后续由于这个原因未对名字作出修改,在此说明

复杂度分析

​ 代码量分析如下

​ 可以看到总体上代码量比较平均,Calculator类和Main类的代码量相对偏多,这是由于calculator内包含了所有的计算方法以及多项式化简,而main类内则有那些当时没想好怎样归到工具类内的方法,这些原因导致代码量偏多。

​ 类复杂度分析如下:

Class OCavg OCmax WMC
expressionclass.Factor n/a n/a 0
expressionclass.Oper 1 1 5
expressionclass.PowerFun 2.43 11 17
main.Main 4.67 9 14
util.Calculator 4.8 10 24
util.Lexer 1.38 3 11
util.PreProgressor 2.75 7 11
util.TreeCreator 5.33 8 16

​ 可以看到比较复杂的类其实也是上面行数比较多的,而TreeCreator内则是涉及到了中缀转后缀的操作,所以复杂度较高。

​ 方法复杂度分析如下

Method CogC ev(G) iv(G) v(G)
expressionclass.Oper.Oper() 0 1 1 1
expressionclass.Oper.Oper(String) 0 1 1 1
expressionclass.Oper.getVar() 0 1 1 1
expressionclass.Oper.setVar(String) 0 1 1 1
expressionclass.Oper.toString() 0 1 1 1
expressionclass.PowerFun.PowerFun() 0 1 1 1
expressionclass.PowerFun.PowerFun(BigInteger, int) 0 1 1 1
expressionclass.PowerFun.getCoeff() 0 1 1 1
expressionclass.PowerFun.getExp() 0 1 1 1
expressionclass.PowerFun.setCoeff(BigInteger) 0 1 1 1
expressionclass.PowerFun.setExp(int) 0 1 1 1
expressionclass.PowerFun.toString() 15 11 8 11
main.Main.analyze(Lexer) 5 1 4 4
main.Main.main(String[]) 0 1 1 1
main.Main.print(TreeMap<Integer, PowerFun>) 13 6 8 9
util.Calculator.add(TreeMap<Integer, PowerFun>, TreeMap<Integer, PowerFun>) 4 1 3 3
util.Calculator.calculate(ArrayList<Factor>) 11 3 5 9
util.Calculator.mul(TreeMap<Integer, PowerFun>, TreeMap<Integer, PowerFun>) 12 1 6 6
util.Calculator.pow(TreeMap<Integer, PowerFun>, TreeMap<Integer, PowerFun>) 4 1 3 3
util.Calculator.sub(TreeMap<Integer, PowerFun>, TreeMap<Integer, PowerFun>) 4 1 3 3
util.Lexer.Lexer(String) 0 1 1 1
util.Lexer.getElement() 0 1 1 1
util.Lexer.getIndex() 0 1 1 1
util.Lexer.getInput() 0 1 1 1
util.Lexer.getNum() 2 1 3 3
util.Lexer.setElement(String) 0 1 1 1
util.Lexer.setIndex(int) 0 1 1 1
util.Lexer.step() 5 2 3 3
util.PreProgressor.delete(String) 0 1 1 1
util.PreProgressor.doubleSignProgress(StringBuilder, Integer) 15 6 8 8
util.PreProgressor.format(String) 2 1 1 2
util.PreProgressor.replace(String) 0 1 1 1
util.TreeCreator.niTransPost(ArrayList<Factor>) 21 6 11 11
util.TreeCreator.opCompare(String, String) 0 1 1 1
util.TreeCreator.opOrder(String) 1 6 1 6

​ 总体上的复杂度不高,高复杂度方法(上文标粗的方法)基本上是由于分支过多导致的,比如PowerFun的toString复杂度主要来源于对化简的分类讨论,TreeCreator方法中的niTransPost方法则是因为对符号的优先级判断导致的复杂度上升。

bug分析(自己)

​ 第一次作业的中强测以及互测中均未出现bug。

bug分析(他人)

​ 在这次互测中,我主要采用的是随机轰炸为主,精准打击为辅的方法。最终查出一个别人的bug:结果为“-1”会输出“-”,显然这是化简过头了忘了考虑特殊情况。在我自己的程序中并未出现这种问题,因为在toString方法的编写中已经考虑到了这种情况。

第二次作业

作业分析

​ 第二次作业增加了三角函数,自定义函数,求和函数,直接将难度抬升了一级。但很可惜的是,第二次作业时我依旧没完全弄懂递归下降,只明白了”下降“但不知道怎么将分析好的因子”回归“地计算回来。而继续采用原架构的原因除了不懂递归下降且时间紧迫外,还有一个原因是我在第一次作业时已经对第二次作业有了个基本的构想,这个在下文会详谈。

​ 我的处理方法是这样的

  • 对三角函数,将其当成一个特殊的一元运算符,原串中”sin“替换为”s”,“cos”替换为“c“。由于sin,cos运算部分必定被一对括号括起来,所以在运算时,对括号处理完后再次判断栈顶是否为三角运算符,若是则弹出后进入写好的三角函数方法处理即可。

  • 对自定义函数和求和函数,建立相应的类DiyFun和SumFun,并编写对应的展开方法。展开策略是非常暴力的直接字符串替换,其间考虑到防止形参x和实参x的混用,在替换过程中采用了另一个不同于变量的字母当作占位符(本次作业中为w),最后统一将其换成x。为了防止出现暴力替换后运算优先级变化,每个被替换的部分外围用一对括号包围。

  • 计算部分的基础因子更改为\(ax^b\prod_{i}sin^{c_i}(E_i)\prod_{j}cos^{d_j}(E_j)\),三角函数专门建立三角函数类,里面包含计算因子。由于没想到合适的key,所以采用HashSet存储计算结果。

    加入了三角后,化简的方法理论上有无数种,在这次作业中,保留了之前的多项式化简,我经过计算和统计分析(算了下期望),最终只采用了化简效率最高的\(sin^2(E)+cos^2(E)=1\)。化简部分写在了AfterProgressor类里。

    本次作业类图如下

​ 可以看到,这次的类图比上次要复杂得多,但是模块之间的耦合关系依旧很弱的优势依旧保留了下来,但是工具类越来越多也导致整个代码的维护难度增大。而整体类图也可以看到分成了两个部分,左半部分是输入与预处理部分,右半部分是计算与化简部分,结构相对清晰。

​ 本次对有变动的类文字解释如下(参照第一次)

解释
Main 主类,将print方法写到了AfterProgressor中,简化主类
PowerFun 幂函数类,增加对equals方法重写用于化简中同类项判断
DiyFun 自定义函数类,对自定义函数的定义及调用进行封装与处理
SumFun 求和函数类,对求和函数进行封装与处理
TriFun 三角函数抽象类,对三角函数共性的部分进行封装处理
Sin 正弦函数类,对正弦函数进行封装处理
Cos 余弦函数类,对余弦函数进行封装处理
CalFactor 计算因子类,将上面提到的基本计算因子进行封装,便于计算与化简
AfterProgressor 后处理器,对运算结果进行化简与输出
Cloner 克隆器,进行深克隆
PreProgressor 预处理器,增加对自定义函数与求和函数展开的方法
Calculator 计算器,增加三角函数计算与运算中的临时化简方法

复杂度分析

​ 代码量分析如下

​ 明显地看到这次代码量直接翻了一番,且代码集中在计算与化简部分。

​ 类复杂度分析如下

Class OCavg OCmax WMC
expressionclass.CalFactor 5.09 17 56
expressionclass.Cos 5 9 10
expressionclass.DiyFun 1.8 3 9
expressionclass.Factor n/a n/a 0
expressionclass.Oper 1 1 3
expressionclass.PowerFun 2.25 11 18
expressionclass.Sin 5.5 10 11
expressionclass.SumFun 2 3 4
expressionclass.TriFun 1.57 5 11
main.Main 3 4 6
util.AfterProgressor 6.29 11 44
util.Calculator 5 11 40
util.Cloner 1 1 1
util.Lexer 1.6 3 8
util.PreProgressor 4.6 12 23
util.TreeCreator 5.33 9 16

​ 这次爆红(表中为标粗)的类大幅增加,Calculator类在原本复杂度较高的情况下又增加了三角的计算,使复杂度进一步升高。而化简部分为了化简彻底,采用了循环暴力搜索的方法,这使得其作为一个新类复杂度却很高。

​ 方法复杂度分析如下

Method CogC ev(G) iv(G) v(G)
expressionclass.CalFactor.CalFactor() 0 1 1 1
expressionclass.CalFactor.CalFactor(PowerFun, HashSet<Sin>, HashSet<Cos>) 0 1 1 1
expressionclass.CalFactor.equals(Object) 29 9 13 13
expressionclass.CalFactor.getCoss() 0 1 1 1
expressionclass.CalFactor.getPowerFun() 0 1 1 1
expressionclass.CalFactor.getSins() 0 1 1 1
expressionclass.CalFactor.hashCode() 0 1 1 1
expressionclass.CalFactor.mulCalFactor(CalFactor) 22 6 11 11
expressionclass.CalFactor.selfClean() 20 1 14 14
expressionclass.CalFactor.toString() 0 1 1 1
expressionclass.CalFactor.toString(boolean) 47 3 19 22
expressionclass.Cos.Cos(HashSet<CalFactor>, long) 0 1 1 1
expressionclass.Cos.toString() 21 4 10 11
expressionclass.DiyFun.DiyFun(String) 2 1 3 3
expressionclass.DiyFun.getExpr() 0 1 1 1
expressionclass.DiyFun.getName() 0 1 1 1
expressionclass.DiyFun.getParam() 0 1 1 1
expressionclass.DiyFun.plug(DiyFun) 4 2 3 3
expressionclass.Oper.Oper(String) 0 1 1 1
expressionclass.Oper.getVar() 0 1 1 1
expressionclass.Oper.toString() 0 1 1 1
expressionclass.PowerFun.PowerFun(BigInteger, long) 0 1 1 1
expressionclass.PowerFun.equals(Object) 0 1 1 1
expressionclass.PowerFun.getCoeff() 0 1 1 1
expressionclass.PowerFun.getExp() 0 1 1 1
expressionclass.PowerFun.hashCode() 0 1 1 1
expressionclass.PowerFun.setCoeff(BigInteger) 0 1 1 1
expressionclass.PowerFun.setExp(long) 0 1 1 1
expressionclass.PowerFun.toString(boolean) 16 11 8 12
expressionclass.Sin.Sin(HashSet<CalFactor>, long) 0 1 1 1
expressionclass.Sin.toString() 22 5 10 12
expressionclass.SumFun.SumFun(String) 0 1 1 1
expressionclass.SumFun.unfold() 4 2 3 3
expressionclass.TriFun.equals(Object) 15 5 11 11
expressionclass.TriFun.getCalFactors() 0 1 1 1
expressionclass.TriFun.getExp() 0 1 1 1
expressionclass.TriFun.hashCode() 0 1 1 1
expressionclass.TriFun.mulEqual(TriFun) 0 1 1 1
expressionclass.TriFun.setCalFactors(HashSet<CalFactor>) 0 1 1 1
expressionclass.TriFun.setExp(long) 0 1 1 1
main.Main.analyze(Lexer) 5 1 4 4
main.Main.main(String[]) 1 1 2 2
util.AfterProgressor.checkSquareAble(CalFactor, CalFactor, Sin, Cos, boolean) 12 6 6 6
util.AfterProgressor.print(HashSet<CalFactor>) 20 6 9 10
util.AfterProgressor.selfSimply(CalFactor) 27 6 11 11
util.AfterProgressor.setSelfClean(HashSet<CalFactor>) 1 1 2 2
util.AfterProgressor.setSimply(HashSet<CalFactor>) 3 1 3 3
util.AfterProgressor.simplyAll(HashSet<CalFactor>) 6 4 3 4
util.AfterProgressor.squareTriFun(CalFactor, CalFactor) 40 11 17 17
util.Calculator.add(HashSet<CalFactor>, HashSet<CalFactor>) 8 4 5 5
util.Calculator.calculate(ArrayList<Factor>) 14 5 6 10
util.Calculator.cosine(HashSet<CalFactor>) 5 3 3 4
util.Calculator.mul(HashSet<CalFactor>, HashSet<CalFactor>) 16 6 6 6
util.Calculator.pow(HashSet<CalFactor>, HashSet<CalFactor>) 1 1 2 2
util.Calculator.sine(HashSet<CalFactor>) 5 3 3 4
util.Calculator.sub(HashSet<CalFactor>, HashSet<CalFactor>) 8 4 5 5
util.Calculator.tempSimply(Stack<HashSet<CalFactor>) 2 1 3 3
util.Cloner.deepClone(Object) 0 1 1 1
util.Lexer.Lexer(String) 0 1 1 1
util.Lexer.getElement() 0 1 1 1
util.Lexer.getNum() 2 1 3 3
util.Lexer.setElement(String) 0 1 1 1
util.Lexer.step() 5 2 3 3
util.PreProgressor.delete(String) 0 1 1 1
util.PreProgressor.diySumUnfold(String, ArrayList<DiyFun>) 25 8 8 12
util.PreProgressor.doubleSignProgress(StringBuilder, Integer) 15 6 8 8
util.PreProgressor.format(String) 2 1 1 2
util.PreProgressor.replace(String) 0 1 1 1
util.TreeCreator.niTransPost(ArrayList<Factor>) 28 6 14 14
util.TreeCreator.opCompare(String, String) 0 1 1 1
util.TreeCreator.opOrder(String) 1 5 1 5

​ 爆红的方法依旧集中在计算和化简部分,两个部分均涉及到判断同类项,涉及到CalFactor,PowerFun和TriFun三者之间equals方法的相互调用,导致复杂度急剧上升。

bug分析(自己)

​ 本次作业的中强测中未出现bug,在互测中被测出一个bug,而这个bug的产生相当有意思。

​ 可以发现,上文中的计算基本因子中,三角内部我用的是\(E\)而不是\(x^n\)。实际上,这份作业已经能够支持三角内带表达式的情况了,所以我顺便也将优化一起做了,这个bug就来自”负项推迟出现“这一优化,具体问题出现在TriFun抽象类的equals方法中。

​ bug样例:

0
sin(-1)

​ 优化原理是这样的:遍历三角内所有CalFactor,找到第一个非负项输出并用一个temp变量保存该项,之后在用增强for循环输出,输出到该项时则跳过。而本次作业中三角内部仅可能出现一项,当该项为负时,temp没有被更改,其仍为初始化时的值。而我原本采用的初始化是直接new一个新的对象,导致在后面输出部分调用equals方法判断是否和temp相同时,因为CalFactor的equals方法需要调用TriFun的equals方法,而此时temp的每项元素均为空,所以会报空指针异常导致RE。这个bug就算这次没出现,在下次也会在内部表达式每项均为负时出现。当然,这个bug被hack,归根见底是测试没做全面,上面这个样例相当容易构造而且属于一个基础类。

​ 修改方法也很简单,将temp初始化为null,然后在判断逻辑上加一句temp!=null即可。

​ 观察方法复杂度分析表可知,出bug的方法TriFun.equals圈复杂度偏高(11,大于10算偏高)。在所有方法中复杂度处于中上水平,可见复杂度和出bug的概率总体上还是呈正相关的,以后需要多加注意对复杂度的控制。

bug分析(他人)

​ 本次作业由于失去了评测机,我主要是采用观察别人的代码构造样例以及使用我自己用过的出过bug的样例进行测试。显然,直接上手看代码的效率是相对较低的,于是我想到一个方法:把代码下下来后看idea的warning信息,这部分信息经常被忽视但却点出了代码中可能出现问题的地方。这样我可以首先通过warning信息去有针对地找问题,极大提高效率。

​ 本次作业中整个房间未发现除我以外其他人的bug。

第三次作业

作业分析

​ 第三次作业增加了三角函数内部因子嵌套和自定义函数多层调用,但在上文提到,第二次作业已经支持三角函数内嵌套因子,因此我只需要解决后面的问题即可。

​ 而解决后面那个问题的方法很简单:循环展开

​ 原串可一般表示为\(E_1f(f,g,h)E_2\),其中f,g,h均为函数,第一次展开后会得到\(E_1(f,g,h)_fE_2\)\((f,g,h)_f\)表示按f代入后的结果,再展开一次后则变为\(E_1(E_f,E_g,E_h)_fE_2\)此时如还存在自定义函数可继续对其展开,由于调用层数有限,最终总是能展开到式子里没有自定义函数,此时便完成了展开。求和函数的展开同理。
tips:这种最原始的展开实际上无法处理求和函数内套求和函数的情况,如需实现该需求则需要进行一定的修改,下文也会提到

​ 所以除去新增的优化部分,第二次作业到第三次作业的代码增量很小。

​ 第三次作业中新增的优化主要有以下几点,优化的选择基于期望收益与投入考虑。

  • \(aE_1sin^2(E_2)+bE_1cos^2(E_2)=(a-b)E_1sin^2(E_2)+bE_1\)(增强版\(sin^2(E)+cos^2(E)=1\)
  • \(2sin(E)cos(E)=sin(2E)\)
  • \(\pm(1-sin^{2}(E))=\pm cos^{2}E\)
  • \(\pm(1-cos^{2}(E))=\pm sin^{2}E\)
  • \(\pm(cos^2(E)-sin^2(E))=\pm cos(2E)\)
  • \(sin(-E)=-sin(E)\)
  • \(cos(-E)=cos(E)\)
    本次作业的类图如下:

​ 可以看到除了AfterProgressor加了一堆优化方法之外,其他地方和第二次作业并无显著差别,故不再赘述。当然这里应该检讨一下自己为了减少代码量而把sin和cos的toString方法提到抽象类来,通过instance of来确定不同的部分如何输出。实际上这么做个人认为是非常不好的,一旦三角种类增多,这边if判断就会跟着增多,复杂度一下就上来了,最好方式还是在每个三角函数类里单独重写toString方法,虽然重复代码会很多,但这样方便扩展。

​ 本次作业中有变动的类的解释如下(相较于第二次作业):

解释
AfterProgressor 后处理器,新增一大批化简方法,具体功能见前文
Cloner 新增自己设计的浅克隆方法,用于计算与化简中使用
Analyzer 分析器,与Lexer一起对输入串进行分析
StringTool 字符串工具,将预处理,计算与化简中常用的字符串处理方法单独封装成一个类,简化代码
TriFun 新增方法用于对三角内式子进行符号化简
其他变动类 主要是为了简化代码,对设计与正确性没有影响,故不再赘述

复杂度分析

​ 代码量分析如下:

​ 可以看到,除去AfterProgressor部分多出来的222行化简代码外,总代码增量仅有12行,虽然我对代码结构进行了一个优化,减少了一部分代码量,但估计真正的代码增量不超过50行,所以这次迭代开发从完成任务的角度而言是比较轻松的。

​ 类复杂度分析如下:

Class OCavg OCmax WMC
expressionclass.CalFactor 4.21 17 59
expressionclass.Cos 1 1 1
expressionclass.DiyFun 1.8 3 9
expressionclass.Factor n/a n/a 0
expressionclass.Oper 1 1 3
expressionclass.PowerFun 2.11 11 19
expressionclass.Sin 1 1 1
expressionclass.SumFun 2 3 4
expressionclass.TriFun 3.2 14 32
main.Main 2 2 2
util.AfterProgressor 6.38 13 102
util.Analyzer 4 4 4
util.Calculator 5.83 10 35
util.Cloner 1 1 2
util.Lexer 1.6 3 8
util.PreProgressor 3.2 7 16
util.StringTool 5 5 10
util.TreeCreator 5.33 9 16

​ 可以发现,除AfterProgressor外,其他类的复杂度较第二次作业并没有很大的变动,这与这次改动主要是在优化部分有很大关系。而由于将所有优化方法全部集中在一个类里,导致AfterProgressor的WMC甚至达到了3位数。这是我们不愿看到的。实际上可以考虑把三角化简做个分类,将化简方法分散到不同的化简类中来降低复杂度。

​ 方法复杂度分析如下:

Method CogC ev(G) iv(G) v(G)
expressionclass.CalFactor.CalFactor() 0 1 1 1
expressionclass.CalFactor.CalFactor(PowerFun, HashSet<Sin>, HashSet<Cos>) 0 1 1 1
expressionclass.CalFactor.equals(Object) 27 9 12 12
expressionclass.CalFactor.getCoss() 0 1 1 1
expressionclass.CalFactor.getPowerFun() 0 1 1 1
expressionclass.CalFactor.getSins() 0 1 1 1
expressionclass.CalFactor.hashCode() 0 1 1 1
expressionclass.CalFactor.mulCalFactor(CalFactor) 22 6 11 11
expressionclass.CalFactor.selfClean() 20 1 14 14
expressionclass.CalFactor.setCoss(HashSet<Cos>) 0 1 1 1
expressionclass.CalFactor.setPowerFun(PowerFun) 0 1 1 1
expressionclass.CalFactor.setSins(HashSet<Sin>) 0 1 1 1
expressionclass.CalFactor.toString() 0 1 1 1
expressionclass.CalFactor.toString(boolean) 47 3 19 22
expressionclass.Cos.Cos(HashSet<CalFactor>, long) 0 1 1 1
expressionclass.DiyFun.DiyFun(String) 2 1 3 3
expressionclass.DiyFun.getExpr() 0 1 1 1
expressionclass.DiyFun.getName() 0 1 1 1
expressionclass.DiyFun.getParam() 0 1 1 1
expressionclass.DiyFun.plug(DiyFun) 4 2 3 3
expressionclass.Oper.Oper(String) 0 1 1 1
expressionclass.Oper.getVar() 0 1 1 1
expressionclass.Oper.toString() 0 1 1 1
expressionclass.PowerFun.PowerFun(BigInteger, long) 0 1 1 1
expressionclass.PowerFun.equals(Object) 0 1 1 1
expressionclass.PowerFun.getCoeff() 0 1 1 1
expressionclass.PowerFun.getExp() 0 1 1 1
expressionclass.PowerFun.hashCode() 0 1 1 1
expressionclass.PowerFun.setCoeff(BigInteger) 0 1 1 1
expressionclass.PowerFun.setExp(long) 0 1 1 1
expressionclass.PowerFun.toString() 0 1 1 1
expressionclass.PowerFun.toString(boolean) 16 11 8 12
expressionclass.Sin.Sin(HashSet<CalFactor>, long) 0 1 1 1
expressionclass.SumFun.SumFun(String) 0 1 1 1
expressionclass.SumFun.unfold() 4 2 3 3
expressionclass.TriFun.accessCheck() 3 2 5 5
expressionclass.TriFun.checkMinusAndNegate() 6 3 4 5
expressionclass.TriFun.equals(Object) 14 5 10 10
expressionclass.TriFun.getCalFactors() 0 1 1 1
expressionclass.TriFun.getExp() 0 1 1 1
expressionclass.TriFun.hashCode() 0 1 1 1
expressionclass.TriFun.mulEqual(TriFun) 0 1 1 1
expressionclass.TriFun.setCalFactors(HashSet<CalFactor>) 0 1 1 1
expressionclass.TriFun.setExp(long) 0 1 1 1
expressionclass.TriFun.toString() 29 5 10 16
main.Main.main(String[]) 1 1 2 2
util.AfterProgressor.checkSquareMinusAble(CalFactor, CalFactor, Sin, Cos) 0 1 1 1
util.AfterProgressor.checkSquarePlusAble(CalFactor, CalFactor) 13 1 8 8
util.AfterProgressor.checkSquareSingleAble(CalFactor, CalFactor, TriFun, boolean) 20 6 8 8
util.AfterProgressor.eliminationSquarePlus(CalFactor, CalFactor, Sin, Cos, int) 18 6 9 9
util.AfterProgressor.oneOpSquareTriMerged(CalFactor, TriFun) 16 6 8 8
util.AfterProgressor.oneSubCosSquare(CalFactor, CalFactor) 20 1 13 13
util.AfterProgressor.oneSubSinSquare(CalFactor, CalFactor) 20 1 13 13
util.AfterProgressor.print(HashSet<CalFactor>) 20 6 9 10
util.AfterProgressor.selfSimply(CalFactor) 35 7 13 13
util.AfterProgressor.setSelfClean(HashSet<CalFactor>) 1 1 2 2
util.AfterProgressor.setSimply(HashSet<CalFactor>) 4 1 4 4
util.AfterProgressor.simplyAll(HashSet<CalFactor>) 24 13 9 13
util.AfterProgressor.simplyOneOpSquareTri(CalFactor, CalFactor, TriFun) 6 1 4 4
util.AfterProgressor.simplySquareTriFunMinus(CalFactor, CalFactor) 32 7 11 11
util.AfterProgressor.squareTriFunMinus(CalFactor, CalFactor) 4 1 5 5
util.AfterProgressor.squareTriFunPlus(CalFactor, CalFactor) 4 3 4 4
util.Analyzer.analyze(Lexer) 5 1 4 4
util.Calculator.add(HashSet<CalFactor>, HashSet<CalFactor>) 8 4 5 5
util.Calculator.calculate(ArrayList<Factor>) 14 4 6 10
util.Calculator.mul(HashSet<CalFactor>, HashSet<CalFactor>) 16 6 6 6
util.Calculator.pow(HashSet<CalFactor>, HashSet<CalFactor>) 9 1 6 6
util.Calculator.sub(HashSet<CalFactor>, HashSet<CalFactor>) 1 1 2 2
util.Calculator.triFunction(HashSet<CalFactor>, Oper) 15 3 6 7
util.Cloner.deepClone(Object) 0 1 1 1
util.Cloner.myShallowClone(CalFactor, CalFactor) 0 1 1 1
util.Lexer.Lexer(String) 0 1 1 1
util.Lexer.getElement() 0 1 1 1
util.Lexer.getNum() 2 1 3 3
util.Lexer.setElement(String) 0 1 1 1
util.Lexer.step() 5 2 3 3
util.PreProgressor.delete(String) 0 1 1 1
util.PreProgressor.diySumUnfold(String, ArrayList<DiyFun>) 8 1 5 5
util.PreProgressor.doubleSignProgress(StringBuilder, Integer) 15 6 8 8
util.PreProgressor.format(String) 2 1 1 2
util.PreProgressor.replace(String) 0 1 1 1
util.StringTool.catchBracket(int, String) 6 3 3 5
util.StringTool.spliter(String) 6 1 6 6
util.TreeCreator.niTransPost(ArrayList<Factor>) 28 6 14 14
util.TreeCreator.opCompare(String, String) 0 1 1 1
util.TreeCreator.opOrder(String) 1 5 1 5

​ 同理,原有的方法复杂度并无太大变化,而新增的化简方法每个复杂度都爆红,与化简统一采用暴力搜索方式有关,而这些新增的化简方法都很复杂也导致了AfterProgressor的复杂度急速上升。

bug分析(自己)

​ 本次中强测和互测中未发现bug,但是强测没有把我的优化完全跑出来,后面经询问发现,HashSet这种无序容器在不同的系统上运行结果可能不同,这点并未在指导书或其他官方材料中点出来,某种意义上这算一个小bug,我下次需要注意。

bug分析(他人)

​ 本次互测我和上次一样的策略,也是先看warning,再看代码。同时采用了群友提供的一些数据。

​ 本次互测中我未发现别人的bug,同房中有且仅有一个人被发现一个bug。

​ bug样例:

0
sin((sin(1)+cos(1)))

​ 最终的输出少掉了一层括号,原来是那个人化简的部分出了问题。本次作业中三角函数内为表达式时必须加一层括号,这点在TriFun的toString方法中我已经考虑到了,仅有内部为单独一个幂函数或常数时才不加括号,否则加上外层括号,所以我的程序不存在这个问题。

综合分析:化简

​ 三次作业里,化简一直是一个可扩展性很强的任务。尤其是从第二次作业加入三角函数开始,化简的方法就有无数种。而我从中挑选出了自认为最有效率的几种。这里对三次作业中运用到的化简思路进行一个统一,详细的分析。

  • 合并同类项:这项化简贯穿了三次作业的化简主线。这里的同类项指的是与计算基本因子相比的同类项。即第一次作业中的\(ax^b\)和第二、三次作业中的\(ax^b\prod_{i}sin^{c_i}(E_i)\prod_{j}cos^{d_j}(E_j)\)。当判断为同类项时,则将他们的系数和指数进行处理。

    • 众所周知,合并同类项和合并同类项不能一概而论,对不同的运算,同类项的概念可能是不一样的,比如对加减法而言,\(a_1x^b\prod_{i}sin^{c_i}(E_i)\prod_{j}cos^{d_j}(E_j)\)\(a_2x^b\prod_{i}sin^{c_i}(E_i)\prod_{j}cos^{d_j}(E_j)\)是同类项,指数和底数部分必须完全一致,而对乘法而言,\(a_1x^{b_1}\prod_{i}sin^{c_{i_1}}(E_i)\prod_{j}cos^{d_{j_1}}(E_j)\)\(a_2x^{b_2}\prod_{i}sin^{c_{i_2}}(E_i)\prod_{j}cos^{d_{j_2}}(E_j)\)就算是同类项,只要求底数部分一致即可。所以,对不同运算的判断同类项的方法是不一样的,这也是为什么在TriFun类里面有equals和mulEquals两种方法:equals用于判断加减法同类项,而mulEquals用于判断乘法的同类项,其算法是将原因子的各指数置1后调用equals方法。
    • 上面提到对原因子进行改动,而这个改动我们只希望发生在判断部分,即不希望原式有任何变动,所以需要使用深克隆拷贝一份出来用于比对同类项,也就解释了Cloner这个类产生的原因。
    • 关于深克隆,比较流行的做法是实现Clonable接口后一层一层调用clone方法,我觉得比较麻烦,就采用了另一种方法:序列化。虽然说这种方法不常用,但是序列化方法好处在于它接收和返回的参数都是Object,也就是说写一个Cloner之后可以任意调用,对任何的类都可进行深克隆,复用性很高。
  • 三角化简:从第二次作业加入三角后,三角化简部分就是八仙过海——各显神通。如何从无数的三角公式里找到最高效的那个(些)是我在完成任务时一直在思考的。我最终采用的三角化简策略是这样的:

    • 总方针:能消去三角函数为先,无法消去三角函数则以效率为先。

    • 整个表达式中,最占位置的一定是三角函数,光秃秃的sin(x)就能占据6个字符,所以首要任务一定是消三角项。基于此产生的化简策略有:

      • \(aE_1sin^2(E_2)+bE_1cos^2(E_2)=(a-b)E_1sin^2(E_2)+bE_1\)(增强版\(sin^2(E)+cos^2(E)=1\)

      • \(\pm(cos^2(E)-sin^2(E))=\pm cos(2E)\)

      • \(2sin(E)cos(E)=sin(2E)\):必须注意的是,该条的应用仅限于\(sin(E),cos(E)\)均为一次项时,否则无法达到消项的目的。甚至可能产生负优化。一个经典的例子如下

        0
        6*sin(x)**2*cos(x)**2
        

        使用二倍角公式化简的结果是3*sin((2*x))*sin(x)*cos(x),最终结果甚至比原来长,这显然不是我们想看到的。可以看到此时二倍角公式并无法达到消三角项的作用,所以不应该进行化简。

    • 无法消去三角项的情况下,涉及到的字符变动数一般很少,这时就需要衡量投入与产出来保证效率最大化(显然这里效率不仅指程序效率,也指现实中的工作效率),基于此产生的化简策略有

      • \(\pm(1-sin^{2}(E))=\pm cos^{2}E\)\(\pm(1-cos^{2}(E))=\pm sin^{2}E\):这两条就是上面平方和公式的逆运算,整体思路相同,思考量小且代码部分可复用。
      • \(sin(-E)=-sin(E)\)\(cos(-E)=cos(E)\):这两项相当容易实现,后者直接少一个负号,前者可能在之后的负项提前优化中发挥作用。
  • 负项提前:将负项推迟输出可将负号变成减号,这样可能可以省去开头一个符号,且易实现。

  • 先计算后化简还是先化简后计算:实际上这两个方法都无法做到完全最简,对一部分式子先优化更好,对另一部分式子则是先计算更好。一种比较暴力的策略是遍历所有化简顺序输出最短的那个,但这种说法极有可能导致超时。所以我采取的策略是先计算后化简,但当出现三角运算符时先将已有式子进行化简后放入三角中。

    • 这么做原因有二。一是三角函数内部的式子几乎不可能再被改变了,再不化简就没机会化简了。二是化简将三角内部式子的形式进行了统一,方便之后对同类项的判断以及化简。
  • 化简策略整合:化简策略使用顺序按消去字符量由高到低排列;当任一化简策略成功时,再次使用该策略,同时在所有策略执行完毕时从头开始再执行一遍所有策略。

    • 第一点主要针对的情况是一个式子可能适用于多种化简策略,但使用了其中一种之后就没法再使用其他的,这时就应该使用化简效率最高的策略进行化简
    • 第二点主要针对的情况是一个式子使用了其中一种策略之后又符合另一种策略的条件,这样便可以达到现有策略下的最简。当然,这种算法带来的一个巨大问题就是时间复杂度非常高。

宏观架构分析

​ 说实话,这三次作业架构的形成相当具有戏剧性。刚开始采用的临时架构最后竟然一条路走到黑,还算不错地完成了任务。架构迭代历程及想法如下:
​ 总体而言,架构采用了“输入-运算-输出”的三段式处理,三部分之间通过装有各因子的容器进行数据传输。相互之间解耦合。

  • 第一次作业:因不懂递归下降,采用后缀表达式的老方法。后缀表达式计算就是最基本的数据结构那套算法,在此不再赘述。同时为递归下降的重构留好了后路。PreProgressor,Calculator组件均可复用到递归下降中。而这套结构在构思时已经想好了之后增加三角函数的处理方法:视为特殊运算符入栈。给自己做好了两手准备。

    • 我们知道三角函数和一般运算符本质上也是种映射关系。例如正弦函数和加号可定义为\(f(x)=sin(x)和g(x,y)=x+y\),那既然一般的加减乘除能够用后缀表达式进行计算,那么三角函数也可以使用后缀表达式进行计算,只需要调整一下符号栈和计算因子栈的弹出规则就行。三次作业的事实证明,这种做法是可行的。而且理论上,这种思想可以沿用到其他的初等函数甚至更复杂的函数中。

    • 可以发现第一次作业直接采用TreeMap<Integer, PowerFun>作为计算结果其实是对迭代不友好的,这种结构仅支持多项式,一旦公式复杂起来,就必须进行重构。所以从完成任务的角度看,这种写法简洁而高效,但从长远来看,这增加了未来的工作量。

  • 第二次作业:此时对递归下降一知半解,同时由于时间问题,重构可能会导致无法完成任务,于是决定沿用第一次的架构,实现之前的设想,同时对自定义函数和求和函数进行处理。

    • 对递归下降而言字符串处理可能不是个好办法的原因有二:一是直接进行建模对函数进行递归更加直接,二是字符串展开会导致原来的递归深度进一步加深,不利于提速。但是对后缀表达式而言字符串替换则是比较好的,因为对表达式建模再用后缀去解,其效果和时间与直接展开后一起解决是一样的,不存在递归深度的问题,而且后缀表达式处理多层冗余括号时仅涉及入栈退栈操作,相较于递归调用函数而言是很快的。所以采用了直接字符串替换的暴力方法。
    • 此时已经大致猜到了第三次作业会在三角里放表达式,所以直接做出了三角函数放表达式的版本,而且在我的架构下,三角里面放什么东西对代码量和思考量并不会造成太大影响,影响较大的则是分析出来的复杂度。
  • 第三次作业:终于在舍友的帮助下搞懂了递归下降的“回归”部分,但这次的任务要求在第二次作业时已完成了大半,没有必要进行重构了。也就作罢,最终一条路走到黑。

    • 事实上,当时我想的是第三次作业是三角里套表达式加格式检查。这样我就可以在格式检查部分实践递归下降算法来检验一下我的学习成果(因为后缀表达式自身不带格式检查功能),但没想到最后却是三角套表达式加多层调用,这样我没办法硬塞一个递归下降进去,而为了一个算法推倒已有的几近完成的代码显然是不明智的,所以就继续沿用了之前的架构,整个架构与指导书背道而驰。也正应了前文提到的关键词“叛逆”。

​ 从整体架构上分析,这次架构存在一个比较大的问题就是计算时间总体上偏长,虽然解决了递归下降问题,但一旦式子长起来,尤其是求和函数上下界差距大点,运算时间就会大幅上升。这个问题其实主要来源于暴力展开的层数多起来后,Calculator需要处理非常多的冗余括号,这样便极大地浪费时间,但是加括号是为了保证运算优先级不被打乱,保证正确性。在不动架构的情况下,只能先牺牲这部分时间。一个比较明显的例子如下:

0
sum(i, 0, 220, (sin((i*x))**2+cos((i*x))**2))

​ 这个式子基本上卡在公测数据长度的上限位置左右,用递归下降程序需要5s左右,而我的程序却需要长达15s。

​ 这次的架构在低耦合性上做得不错,每个类单独抽出来都可以作为一个模块安插到别的程序去使用。但是却没有很好地贯彻OOP的思想。原因有二,其一是刚上手面向对象,对其还不太熟悉,思维仍停留在面向过程,写出的代码中仅有对计算因子与解析因子的封装上体现了面向对象的思想,而工具类的思想在这实质上是披着面向对象的皮的面向过程。其二则是后缀表达式本身相较于递归下降而言,对表达式解析的粒度比较小,也更偏向于面向过程。不过从效果上看,这套架构还是很好地完成了任务。

​ 在我的架构设计上,总是会“超前”设计或者预留一部分功能,并且从这三次作业的情况来看基本上每次都猜对了。虽然在现实的产品开发环境中,预留功能可以,但最好不要做超前设计防止客户一个需求就把你之前的设计全给扬了。但是在面向对象课程中,我觉得这是可以的,因为超前设计肯定是建立在本周你时间充裕的情况下,这样有利于我更灵活地调配自己的时间,并且本身代码量不大,就算预测失误代价也不大。所以在我的架构设计中总会看到一些本不属于这次作业的设计。就以最后设计的终稿为例,实际上仅需稍加改动便可直接支持求和函数与自定义函数的互相嵌套,所以总体上,这套架构对需求变动的适应性和灵活性还是很好的。

总结心得

​ 面向对象的第一单元在此要画上个句号了。

​ 在理论课上,我学到了基本的面向对象思想,而且还能和同学们进行激烈的思想碰撞。OOP的思想确实是“只能意会,不能言传”,光上课听,不管老师讲得多好,我能了解到的只有皮毛,只有亲身实践才能彻底明白OOP。这次作业内面向对象和面向过程的混合其实挺好地反应了我的思想的转变过程。虽然很多人都说面向对象更符合人类的思考问题的方式,但私以为,面向过程和面向对象只是看待问题的两种方式,倒不存在谁更符合人类思想一说。而且在现在的程序中,两种思想都有用武之地。而OO课的倾向自然是使用面向对象的思想,而这次作业算是面向对象一个比较好的例子,他既能让已经了解面向对象的人使用面向对象思想非常流畅地完成,也能照顾到像我一样没办法很快接受面向对象的人用面向对象加面向过程的“四不像”来完成,而且完成效果与前者并无二致。

​ 在实践作业中,我的架构思想得以实现。同时也体会到了迭代开发的一个简要流程。而第一次实践作业便给了我一个下马威,当时的我就是呆坐在电脑前,看着递归下降的资料,毫无思路,最终只能说先用数据结构中学到的后缀表达式先挨过第一次作业再说,没成想最后一条路走到黑。不得不说这次架构的形成充斥着诸多被逼走投无路后急中生智想出来的主意,例如改造栈弹出规则等,也算是从另一个角度学习了面向对象和复习了数据结构。

​ 按照“千里眼”(就是往届博客,我更喜欢这种叫法,以后可能沿用)的数据看,第一单元的代码量比较大的(是否为最大有待考究),本单元作业最终实现时1500+的体量我也着实没想到(我一直以为700-800行就能搞定了),但最终我完成了,最终完成的那一刻有一种无法言说的解脱感。

​ 从第一单元OO课上我还学到一样东西,就是时间管理以及懂得取舍。OO这门课神奇的地方在于它用各种规则想让你把全部时间心甘情愿地花在他身上。显然这是不可能的。生活不止眼前的OO,还有天边的星光与手中的长枪。这次作业其实到最后最烧时间的地方就是做化简,但是化简除了依托的数学公式不同外,其余流程在不同的化简之间并没有很大的区别,而这次作业化简可以无限地做下去。所以,从我的视角来看,从学习知识的角度上来看,过多做化简是没有太大意义的,甚至某种程度上违反了“不要重复造轮子”的原则。这就是我为什么化简只做了上面这部分化简的原因。OO教会了我舍去一些不必要的操作以换取更有价值的对时间的利用,提高整体的效率。

​ 综上,第一单元的面向对象课程我收获很多。但显然我的脚步不能在这停下,后面依旧有未知的挑战在等着我,我将尽我所能去克服他们。

posted @ 2022-03-23 19:13  fishlife  阅读(75)  评论(2编辑  收藏  举报