BUAAOO第一单元总结
拿荣文戈老师做了一波表情包(逃),下面是OO第一单元作业MATLAB表达式求导的总结。
一、代码静态分析
1、初入山门——第一次作业
1.1类图
由于初次接触面向对象,加之第一次作业任务并不复杂,因此我并没有设计太多的类结构。花费了精力进行多项式类的封装,力求使程序中“面向过程”的部分最少。
1.2复杂度分析
多项式类采用ArrayList进行存储,而在加入新元素或求导时都进行了多次遍历甚至多层遍历,造成了复杂度的上升。Parser由于涉及复杂的正则判断输入,复杂度很高。
1.3心得体会
初次接触面向对象,在封装上煞费苦心疯狂爆肝,自我感觉做得不错;
正则令人头大,虽然由于自己考虑不全面出了一些问题,思路自认为不错,留待后文详述。
2、渐入佳境——第二次作业
2.1类图
本次作业设计了分层次的类结构,但主要是一个类作为另一个类的元素,并未用到继承等高级特性。
2.2复杂度分析
复杂度依然来自于ArrayList神教以及复杂正则处理。
2.3心得体会
任务难度比第一次作业明显高了一个层次(主要体现在正则更难写了……)。在项的类型增多之后,封装的威力逐渐显现。封装的过程虽然耗时、枯燥,但为之后的开发提供了巨大的便利,能够让我把精力更多地放在项目的逻辑结构上,不必纠结于实现的细节。
3、当场去世——第三次作业
3.1类图
第三次作业过于爆炸,我建立了很多的类……由于混用了接口和继承,看着有点不伦不类,姑且当做一次对面向对象高级特性的尝试吧(雾)……
3.2复杂度分析
出乎意料,由于层次化了类结构,平均复杂度反而比前次作业有所降低。另外ArrayList神教的弊端再次显现,总复杂度已经爆炸,如果我再加一些三角函数的优化,可能就妥妥的TLE了……
3.3心得体会
第三次作业的类结构比较复杂,我尝试采用继承和接口组织各个项类。但由于理解不深,出现了一些问题,比如出现父类和子类实现同一个接口这样的无用功。关于继承和接口的一些思考,将在后文进一步叙述。
二、HACK & HACKED
1、“面向WF编程”
曾看到同学吐槽,OO课成了“面向WRONG FORMAT编程”,大家的精力都耗费在了处理输入格式上,真正的OOP结构反倒不被重视。从培养面向对象思想的教学目标来看,我也认为这样有些欠妥。不过,如果说OO这门课把培养工程能力也作为一个教学重心,这样的训练可能确实是卓有成效的。
回到这三次作业,最可能出现的两种bug是计算错误和格式错误。鉴于求导只用到了为数不多的几个公式,通过中测的程序基本不会出现计算错误(我因为粗心出现过一个,捂脸),于是互测就完全成了WF大战。很多时候大家对自己的WF被人抓着不放感到不忿,但其实WF往往是由于严重的逻辑问题产生的,比如:
-某同学将一个考虑了所有特殊情况的单项正则表达式简单堆叠,完全没有考虑项之间的连接情况。他的程序会将“x3”判为正确,因为“x”和“3”都是合法项;
-我在第二次作业中设计了一个标志某乘积因子是否可以直接加入多项式的布尔变量,但忘记在识别到乘号时置位,导致判断“x*”为正确输入;
-我在第三次作业判断三角函数嵌套因子是否为单因子时,出现了非常严重的、难以在不该改变算法情况下修复的逻辑bug。
在每一次作业中,我都为输入模块的逻辑绞尽脑汁,但总会被人抓到自己没有想到的特殊情况,我在受挫的同时也学到了很多。在与复杂输入逻辑鏖战的同时,我也感受到了OOP思想的威力,如果没有前期完备的多项式封装实现,根本不可能这样没有后顾之忧地思考业务逻辑。“面向WF编程”,确实带给了我很多收获。
2、关于测试
构造测试数据对于自己程序的调试是极为重要的。虽说有着“我要是能想到这个测试用例我还会写出这个bug吗?”的灵魂诘问,但先尽自己所能构造全面的测试用例首先可以解决很多无心之失(比如我就被人hack过2个纯笔误,然后后悔自己为什么不好好测试),其次构造用例的过程又是也会启发自己思考以前不曾想到过的新情形。
构造测试数据也是互测的核心。我在第一次互测曾采用读代码肉眼debug模式,第二次作业码量一增加我就炸了,老老实实从讨论区抄写一个对拍器,然后构造用例加特林扫射(我相信大部分人也是这样的)。当然我构造用例的能力有些菜,互测区开放查看用例之后我永远都是对着别人的用例惊叹的那种。(说到这里我要膜向神,三个数据砍了组里11刀,最终hack战绩17/35,OTZ)
3、经验教训
-写完程序一定要好好想些用例自测!
-无所事事的水课闲暇时间不妨完整地读一遍自己的代码,可能有惊喜……
-可以抱大腿和其他同学交流一下,也许他们的脑洞之一就会完美hack你。
三、TRICKS
1、输入处理
前两次作业中,很多同学采用了预处理的方式,先判断整行合法性、合并加减号、去除空白字符,再利用正则逐项读取。这样多次扫描、反复操作字符串的工作给我一种很不舒服的感觉,因此我采用了一遍扫描、边读取边判断的方式,判断逻辑并没有增加,唯一增加的只有正则里的[ \t]*。
首先设计了一个单项的正则。在对首项符号进行特殊分析后,依次交替扫描正则匹配项和运算符。设置了一个记录当前扫描位置的变量lastEnd,每次正则匹配从lastEnd开始,若匹配串起始位置不等于lastEnd说明输入非法,否则更新lastEnd为匹配串末尾,然后向后读取运算符。虽然在具体实现中出现了一些bug,但都是个人瞎作死造成的无伤大雅的小问题,我认为这种状态机式的思路总体上是优美的。
第三次作业我并未对输入逻辑进行大面积重写,改写思路如下:将“(” “sin(” “cos(”也作为可被识别的正则项,当识别到这些项时,调用一个特殊的方法,向后扫描字符串以匹配括号,然后将括号里的内容作为一个表达式递归解析,一切完成后将lastEnd设至外层右括号位置。这样实际上是将诸如“sin(...)”的式子也作为原思路中的项,其他的逻辑无需做任何改动。当然实际实现中又出现了作死的bug……
2、表达式化简
首先关于0、1、同类项的化简,完全可以在项类层面进行。比如向乘积项中增添元素时,若为0则清空项,若为1则不作任何改动,若可与原有因子合并则直接合并,否则才作为新因子加入乘积项。这样做一来可以省去后续处理的麻烦,二来项类在存储时就能尽量缩减元素数量,同时这种内部封装的优化也更符合OOP思想。
关于第二次作业的三角函数化简,主要有三个模式:Axpsinq+2cosr+Bxpsinqcosr+2=Cxpsinqcosr+Dxpsinqcosr+2(此处需要考虑绝对值选择约掉哪一项,暂且认为约掉A);
Axpsinqcosr+Bxpsinqcosr+2=Cxpsinq+2cosr+Dxpsinqcosr+2;Axpsinq+2cosr+Bxpsinqcosr=Cxpsinqcosr+2+Dxpsinq+2cosr.
需要注意的一点是,必须保证|A|+|B|>|C|+|D|才是有效化简,这也是为什么模式一需要判断约掉哪一项。扫描所有项,以此模式两两贪心化简,每成功化简就替换并重新扫描,可以证明循环一定会退出。
此算法不保证为最优化简。
第三次作业的三角化简可照搬以上算法,但由于存储结构不同于第二次,具体实现可能会比较繁琐,我怕TLE所以鸽了。至于什么什么提取括号的狼人操作,我就更肝不动了,告辞.jpg。
3、继承与接口
第三次作业我开始尝试使用继承和接口。我最初的想法是,构建一个Item基类,所有具体类由继承自Item,这些子类均实现Differential接口。但是很快我发现一个问题,由于基类没有实现Differential接口,无法直接对Item类型的引用调用diff方法,这与统一化管理的初衷不符。于是想到给基类实现一个空diff方法,子类的diff方法作为重写存在。但问题又来了……这样设计,子类会继承基类的diff,天然就实现了Differential接口,无法通过是否实现接口这一点来约束子类重写diff,真正有效的接口实现就只有基类。我思索很久并没有想到好的解决办法,最终把这个混用了继承和接口的丑陋无比的程序交了上去。
后来在讨论区看到zsa学长的一些经验分享,才算是对自己的问题有了更清楚的认识。继承主要解决数据的共享,而接口解决操作的共享。我设计中的Item、MutableItem、ImmutableItem类都是没有任何数据的空类,除了让类图好看一些没有更多的作用,这是不合适的,这些都应通过接口实现。我应该设计Differential、Mutable、Immutable三个接口,每个具体项类实现D、M或D、I。java的接口虽然不能用来创建实例,但可以作为引用的声明类型,即允许Differrential temp = new Sin(...)。通过这种机制,可以方便地实现对象统一管理,效果与继承无异。
4、容器
三次作业中我因为懒一直使用ArrayList神教,越做到后面越觉得多层多次遍历是真的难受。看到julao们都在推荐HashMap,我也准备学习一发……
四、结语
以上就是我的第一单元总结。取得了一点小成就,但更多的是被julao吊打的不爽。以往不谏,收拾心情,准备向今后的三个单元出发,还有什么样的冒险在等着我们呢(雾)。
posted on 2019-03-26 09:24 AbyssGazesAlso 阅读(463) 评论(3) 编辑 收藏 举报