OO2021-第一单元作业总结
第一次作业
本次作业,需要完成的任务为简单多项式导函数的求解。
(1)基于度量来分析自己的程序结构
第一次作业比较简单,只有普通的表达式,而且不用判断WF。相关的数据存储完全依靠Factor类实现,内部属性包括private BigInteger coefficient(系数)、private BigInteger power(次方数)。优化也比较容易实现,在这一次仅需要针对系数、指数是否为0、1、-1进行特判,以及最后应该把一个正项放在最前面,从而消除一个“+”。当时没有想到的是x**2还可以优化为x*x,如果做到这一点,那应该可以拿到更高的性能分。
代码规模如下所示:

类分析如下所示:

由上图可以看出,主要的问题出现在Factor类中,它的OCavg(the Average Cyclomatic Complexity)过高。主要是因为该类中的新建、求导、输出方法中判定结构过于复杂,有的是因为设计不良,有的则是因为涉及优化问题。
方法分析如下所示:

由上图可以看出,Factor.Factor(String)和Factor.toString()这两个方法的实现不太优美。究其原因,在Factor.Factor(String)中使用的整数(包括系数、指数)读入方法较为原始,没有整合成更简洁的模式。在Factor.toString()中使用了大量的if-else判断系数、指数的特殊情况(如是否为0,是否为1等等),为一些基本的优化功能服务。
名词解释:
ev(G)基本复杂度,是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。
iv(G)模块设计复杂度,是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。
v(G)圈复杂度,是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。
类图如下所示:(涉及的重构在下文介绍)

优点:
1.将一整个由乘法构成的项处理为包括(系数,函数)的统一形式,较为简洁。
2.化简较为简便。
缺点:
1.还是有些面向过程的残留思想,主要体现在Factor的构造函数中。
2.针对第一次作业进行特化处理,不利于迭代。
(2)分析自己程序的bug
本次程序只找出一个bug,就是处理多个正负号前缀的有符号数时无法准确判断正负,这是因为一开始没有准确理解形式化表达的含义。我对于这种情况的对策是预处理,就是通过replace()方法进行替换。
第一次作业没有出现未通过的公测用例或者被互测发现的bug。
(3)分析自己发现别人程序bug所采用的策略
说来惭愧,这次互测没有发现别人的任何bug;也进行了几次提交,但是没有hack到任何人。总结一下原因的话,一是因为第一次进行这种互测方式,不太好意思放开手脚;二是因为自己当时也没有透彻理解题目的运作方式,找不到有效的测试方法。还是水平不够
后来,在与同学的交流中,我学会了边缘测试等等方法,这为后来的互测打下了基础。
第二次作业
本次作业,需要完成的任务为包含简单幂函数和简单正余弦函数的导函数的求解。
(1)基于度量来分析自己的程序结构
这次作业相较于第一次有了显著的难度飞跃,我愁眉苦脸了两天才完成了任务······
由于涉及括号嵌套,本次采用的方法是构建表达式树,根据加减号将输入字符串处理为各个项,再通过乘号将各个项处理为各个因子,最后处理各个因子,构造不同的常数、幂函数、三角函数、表达式因子(需要递归处理)。
当然,这种方法需要符号预处理 好在第2次没有WF
代码规模如下所示:

类分析如下所示:

由上图可以看出,Factory,Mihanshu,CosNode,SinNode这四个类较为复杂,究其原因,是因为Factory要判断因子的种类进而创建,而另外三个类的新建、求导、输出涉及各种判断。
方法分析如下所示:(部分)

由上图可以看出,就是类分析中提及的四个类里涉及构造的方法实现得不太好,尤其是Factory.addNode(String),它的基本复杂度、模块设计复杂度、圈复杂度全部爆红,因为它调用了太多因子类的构造方法······
类图如下所示:

附一张IDEA自动生成的类图:

优点:
1.对于因子的读入处理得到大大的化简。
2.建树的算法较为简单快捷。
缺点:
1.与常见的建树方法不同,容易受到旧式思维干扰。(为bug的出现打下了坚实的基础)
2.不利于化简,性能没有提升。
3.需要预处理,与日后的WF判断矛盾。
(2)分析自己程序的bug
第二次作业的强测和互测都很惨,原因是出现了两类bug。
一类是WRONG ANSWER型,原因是建立表达式树之后,输出求导结果时的“括号”问题。
由于Node的各个子类是最先建立的,我迷迷糊糊地直接按照“中缀表达式转后缀表达式再建树”的传统建树方式写出了它们的
toString()方法,没有认识到我所使用的“表达式拆为项,项再拆为因子”的建树方式的toString()要求不一样。具体而言,前者是按照括号、乘法、加减法的运算优先级来建立表达式树,而后者是按照表达式、项、因子的逻辑顺序来建立表达式树,这就导致二者建成树再求导之后,因子的顺序不一样,因而对于括号的位置处理也不一样。其实差别不大,但是一点小小的不同就导致部分测试点出现细微的差别,进而失败。我当时没有意识到这一点,在被强测和互测hack之后,经过痛苦的自测才幡然醒悟。
比如,对于加法求导得到的
AddNode节点,输出时返回this.getLeftchild().toString() + "+" + this.getRightchild().toString();而对于乘法求导得到的AddNode节点,输出时应返回"(" + this.getLeftchild().toString() + "+" + this.getRightchild().toString() + ")"。
另一类是TLE型,主要出现在乘法、嵌套特别多时。
一开始以为问题出在建树或者求导上,几经试验发现不是,最后定位于
toString()方法上(又是它)。具体来说,输出时要调用许多的Node子类方法进行大量的特判,从而决定不同形式的返回字符串。比如,乘法节点的左孩子求值为0,那就直接返回“0”,而不是“0*右孩子”。本来可以在toString()前面进行更好的优化,可惜当时被搞得焦头烂额,干脆直接把这些优化注掉了事······
(3)分析自己发现别人程序bug所采用的策略
第2次互测,有所进步,主要采用以下数据:
1.是否具有输出0的保底措施。输入数据1,输出0.(未命中)
2.是否具有处理多级嵌套的能力。输入数据((((((((((x)))))))))),输出1.(命中)
3.是否具有处理符号与括号混合的能力。输入数据中含有-(-(+(类的结构。(命中)
第三次作业
本次作业,需要完成的任务为包含简单幂函数和简单正余弦函数及其嵌套组合函数的导函数的求解。
(1)基于度量来分析自己的程序结构
第3次作业,加入了sin、cos的嵌套处理。由于我把三角函数单独建立了两个子类,而且直接采用“表达式拆为项,项再拆为因子”的建树方式,所以仅仅对三角函数的构造函数SinNode.SinNode(String),CosNode.CosNode(String)、求导函数SinNode.qiudao(),CosNode.qiudao()、输出函数SinNode.toString(),CosNode.toString()进行修改即可轻松完成。
另外,还要判断是否出现了WRONG FORMAT!。为了尽可能地利用第2次作业的成果,我单独设立了WrongFormat类来判断是否出现错误格式,格式无误再进入正常程序(导致更多的开销)。总之,第三次作业的格式判断实现得不够理想!
代码规模如下所示:

类分析如下所示:

由上图可以看出,问题在于Factory,WrongFormat,CosNode,SinNode,Mihanshu这5个类上。分析一下的话,共性原因都是因为大量的判断结构,主要涉及构造、求导等等。
方法分析如下所示:(部分)

由上图可以看出,对应于上文已经分析的5个类,Factory.addNode(String),WrongFormat.wrongpd(String),CosNode.CosNode(String),SinNode.SinNode(String),Mihanshu.Mihanshu(String)这五个方法的复杂度十分不理想。因为它们都调用了大量的其他方法,WrongFormat.wrongpd(String)调用了大量的条件判断以及判断函数等,而其他几个调用了不同节点的构造方法,也拥有大量的判断。
类图如下所示:

附一张IDEA自动生成的类图:

优点:
1.对于正确格式的处理准确率高。
缺点:
1.对于错误格式的处理准确率低,难以应对针对WF的全面测试。如果完成了递归下降分析算法,应该可以更好地判断,可惜写着写着半途而废了。
2.由第二次debug版本迭代而来,没有足够的输出优化。
(2)分析自己程序的bug
中测和互测顺利通过,强测WA了4个点。计算本身没有什么问题,主要的bug还是来自WF判断,典型的问题就是空括号,比如sin(((((())))))这一类的。所以,如果互测也可以提交WF数据的话,那简直无法想象,惨不忍睹······
(3)分析自己发现别人程序bug所采用的策略
第3次互测,也还可以,主要采用以下数据:
1.是否具有输出0的保底措施。输入数据1,输出0.(命中······)
2.是否具有处理多级嵌套的能力。输入数据((((((((((x)))))))))),输出1.(未命中)
输入数据含有sin(cos(x))结构。(命中)
3.是否具有处理符号与括号混合的能力。输入数据中含有-(-(+(类的结构。(命中)
重构经历总结
印象比较深刻的是两次比较大的重构。
1.第一次作业,一开始使用了ArrayList,后来,结合第一次实验课的样例代码以及同学们的交流讨论,又用TreeMap重构了一遍,算是体会了不同容器的使用方法吧。
值得注意的是,为了让一个正项排在第一位,从而省略一个“+”,我对容器进行了排序,不同容器的排序如下所示。
Collections.sort(factorShelf.getShelf(), Comparator.comparing(Factor::getPower)); //ArrayList
Collections.reverse(factorShelf.getShelf());
List<Map.Entry<BigInteger,Factor>> list = //TreeMap
new ArrayList<Map.Entry<BigInteger,Factor>>(shelf.entrySet());
Collections.sort(list,new Comparator<Map.Entry<BigInteger,Factor>>() {
//降序排序
public int compare(Map.Entry<BigInteger, Factor> o1,
Map.Entry<BigInteger, Factor> o2) {
return o2.getValue().getCoefficient().compareTo(o1.getValue().getCoefficient());
}
});
2.从第一次到第二次作业的转变,处理输入的方式重构。
第一次作业,采用正则表达式抓取每一项,通过乘法运算将每一项处理为一个Factor对象,存入容器之中。
当然,仅仅依靠第一次的方法无法处理sin,cos,(),但是重构后的方法可以轻松解决第一次作业。
心得体会
-
要注重自学Java的相关知识。很多时候,为了实现某一目的会盲目地使用自己贫瘠的知识储备,笨拙地堆砌冗杂的代码,最后发现别人用简单而“新奇”的方式解决。
-
IDEA确实是个强大的工具,提示功能帮我开拓了眼界,比如
Collections.sort()的不同写法等等。
-
多向老师、助教、同学请教。在历次作业的实现中,与身边的人交流带给我极大的帮助,尤其是第二次作业时,与学长、同学交流讨论给我带来了很多灵感启发,与舍友的激烈讨论也帮我发现了bug。结合研讨课的经历,我认为必要的交流讨论有助于OO的学习。
-
提前进行完善的构思十分必要。第二次作业进行了较长时间的构思,实现起来比较轻松;反观盲目开工的第一次作业,边写边想边改,很长时间才折腾出结果。
-
OO作业写起来很痛苦,想思路、debug的时候很焦虑,但是最后学到了很多,确实很有收获!!!
-

浙公网安备 33010602011771号