OO 第一单元总结 表达式求导
OO第一单元通过三次递进式的作业让我们实现表达式求导,在这几次作业中我也有很多收获。下面就回顾一下前三次作业中存在的问题。
在个人看来,表达式求导的难点主要有三部分——对输入的处理、表达式的存储结构以及化简。这三次作业我所采用的表达式存储结构都不相同,不过重构的速度还是比较快的(安慰自己)。
第一次作业
在第一次作业中,我的程序总体架构为提取幂函数为Poly对象,建立Polynomial类解析表达式创建幂函数对象,在主类Derivative中进行部分性能优化工作。
① 程序结构分析
UML类图:
第一次作业的幂函数因子较为简单,代码中仅提炼出Poly对象,并在其内部实现求导方法。
Method复杂度:
| Method | ev(G) | iv(G) | v(G) | | -------------------------------------------------- | ----- | ----- | ---- | | Derivative.main(String[]) | 1 | 1 | 1 | | Derivative.printAnswer(HashMap<BigInteger, Poly>) | 4 | 5 | 7 | | Poly.Poly(BigInteger,BigInteger) | 1 | 1 | 1 | | Poly.addCoef(BigInteger) | 1 | 1 | 1 | | Poly.compareTo(Object) | 1 | 1 | 1 | | Poly.derivative() | 1 | 2 | 2 | | Poly.equals(Object) | 2 | 2 | 2 | | Poly.getCoeff() | 1 | 1 | 1 | | Poly.getIndex() | 1 | 1 | 1 | | Poly.hashCode() | 1 | 1 | 1 | | Poly.toString() | 1 | 11 | 12 | | Polynomial.Polynomial(String) | 1 | 1 | 1 | | Polynomial.getPolyitem() | 1 | 2 | 2 | | Polynomial.getfirstitem() | 1 | 9 | 10 |
第一次作业中,化简主要在主类Derivative的printAnswer()方法(将第一个正项优先输出)和Poly类的toString()方法中,printAnswer()涉及对表达式的遍历,基本复杂性高;toString()则包含大量条件语句,多次调用了BigInteger中的方法,循环依赖性高。而表达式解析我采用的是正则+状态机的策略(这个策略三次作业均沿用,感觉还是很舒服的),在getfirstitem()中处理不同类型输入并归一化,代码较为复杂。
② 程序Bug分析
第一次作业在强测、互测中均未出现Bug。
③ 互测采用策略
互测时我采用的策略是“补刀”。我先是用简单的自动测试程序跑了下房间内成员的代码,不过貌似没有出现问题;然而圣杯战争发生了转机——Rider竟然拿下了一血!
于是我一一检查其他成员的代码,发现Berserker的正则存在问题:在正则中匹配的是“[+|-]{0,2}(\\d*.)?x(..[+|-]?\\d+)?
”,其中的“.”可匹配任意字符,就会错误解析表达式。
第一次互测时认真读了屋内其他同学的代码,感觉还是很有收获的。之后两次大部分都读不下去,可读性实在是让人肝疼……放到评测姬里自生自灭吧!另外这次互测也让我意识到了高工同学的可怕之处(误),全屋子就这一个Bug,大佬换着花样hack……卷卷更健康。
④ 对象创建模式
在第一次作业中,我并没有应用对象创建模式,表达式解析就顺便new了新因子,Polynomial类的紧耦合比较严重。
第二次作业
第二次作业增加了三角函数因子,这个时候我也偷偷看了一下去年的第三次作业题目,新增加的嵌套对于表达式的化简无疑是不小的挑战。所以在规划第二次作业的总体架构时,我就面临着一个选择——是要建立终将在第三次作业重构的四元组项;还是要采用“表达式-项-因子”三层结构,为第三次作业留好迭代的空间?
经过一番挣扎(其实并没有,因为我还不会写三层结构),我选择了势必带来重构的四元组形式,也就是将表达式的每一项以“coeff*x**powindex*sin(x)**sinindex*cos(x)**cosindex
”的形式存储,这样处理方式就和第一次作业基本一致。在基本架构沿用第一次作业的情况下,第二次作业给我最大的挑战反而是三角函数的化简,我的化简类Simplify的行数是最多的。
① 程序结构分析
UML类图:
架构基本沿用第一次作业,UML也差不多,不过这次的简化更复杂一些。
Method复杂度:
| Method | ev(G) | iv(G) | v(G) | | ------------------------------------------------------------ | ----- | ------ | ------ | | DeriPrinter.DeriPrinter(HashMap<String, MutiItem>) | 1 | 7 | 7 | | DeriPrinter.getAnswer(HashMap<String, MutiItem>) | 4 | 5 | 7 | | DeriPrinter.printAnswer() | 1 | 2 | 2 | | Item.Item(BigInteger,BigInteger,BigInteger) | 1 | 1 | 1 | | Item.compareTo(Object) | 3 | 3 | 3 | | Item.equals(Object) | 2 | 4 | 4 | | Item.getCosin() | 1 | 1 | 1 | | Item.getPowin() | 1 | 1 | 1 | | Item.getSinin() | 1 | 1 | 1 | | Item.hashCode() | 1 | 1 | 1 | | Item.toString() | 1 | 5 | 8 | | MainClass.main(String[]) | 1 | 1 | 1 | | MutiItem.MutiItem(BigInteger,BigInteger,BigInteger,BigInteger) | 1 | 1 | 1 | | MutiItem.MutiItem(BigInteger,Item) | 1 | 1 | 1 | | MutiItem.addCoeff(BigInteger) | 1 | 1 | 1 | | MutiItem.compareTo(Object) | 1 | 1 | 1 | | MutiItem.derivate() | 1 | 5 | 5 | | MutiItem.equals(Object) | 2 | 2 | 2 | | MutiItem.getCoeff() | 1 | 1 | 1 | | MutiItem.getCosin() | 1 | 1 | 1 | | MutiItem.getIdentity() | 1 | 1 | 1 | | MutiItem.getItem() | 1 | 1 | 1 | | MutiItem.getPowin() | 1 | 1 | 1 | | MutiItem.getSinin() | 1 | 1 | 1 | | MutiItem.hashCode() | 1 | 1 | 1 | | MutiItem.toString() | 1 | 5 | 5 | | ParseExp.MutiItemSign() | 2 | 3 | 4 | | ParseExp.ParseExp(String) | 1 | 5 | 6 | | ParseExp.WrongFormat(String) | 1 | 1 | 1 | | ParseExp.getExpression() | 1 | 2 | 2 | | ParseExp.getFirstitem() | 3 | 13 | 14 | | Simplify.Simplify(HashMap<String, MutiItem>) | 1 | 2 | 2 | | Simplify.exitmatch(BigInteger,Item,Item,Item,HashMap<Item, BigInteger>) | 2 | 3 | 4 | | Simplify.searchcos(HashMap<Item, BigInteger>) | 4 | 6 | 9 | | Simplify.simplify() | 1 | 3 | 4 |
由于一开始并没有明确的化简思路,在第一遍实现了基本求导功能的代码通过中测之后,我为了实现化简功能,又对代码内类的结构做了很多修改,在这一过程中,尽管我确实用类将表达式中的Item与MulItem做了封装,但我却胡乱修改内部方法,这并不符合面向对象的思想,也导致最后代码可读性极差,也为互测时被发现的Bug埋下了祸根。具体表现到MetricReloaded的分析上,就是ParseExp类的getFirstitem()依然是复杂度重灾区,没有采用工厂模式也使得它与其他类的依赖极为严重,优化输出的内容依然有很高的基本复杂性。
② 程序Bug分析
强测中获得性能分为16.545/20,在处理表达式化简时,我所采用的是暴力搜索的方式,每次搜索到可以合并的项就将其合并,然后进行重新搜索。
这种方法显然只能找到一个极优解,却不能找到最优解,表达式项存储顺序对结果也有很大影响。一种更加可行的方式是采用深度优先搜索,不过当时并没有想出具体的实现方法,只好作罢。
互测时被找出来一个Bug,同时发现Berserker的一个Bug(狂战士日常躺枪)。而我的Bug是HashMap的Key写错,在生成MulItem的Key时,我采用的是将指数拼接为String的方式,在指数较大时,可能会导致不同项被当作同一项存入HashMap中,造成求导错误。