目录
第一次作业
要求
对一个包含加减乘,乘方运算,单变量x,含有单层括号的表达式进行去括号化简。
基本思路
预处理
我对于预处理是比较看重的,在这系列迭代中,预处理可以发挥极大作用。首先当然是去多余空白符,把连续加减号进行合并,*+替换为*等基本操作(注意,不止两个加减号相连),然后便来到了我重视预处理的核心原因,如何区分一个加减号,是正负号还是加减号呢?我在预处理就彻底解决这个问题,预处理后,没有正负号,只有加减号,当我们进行正常的预处理后,会有(-1-1)这样的,对于(替换为(0+,输入字符串开头加上0+,然后再进行加减号合并,可以处理普通的这部分,而*-,可以替换为*(0-1)*,这样处理后,就把所有负号解决,剩下就可以安心进行解析,当然,*-也可以不进行预处理,而是在解析的时候再处理,但可以在预处理把问题都解决,让结构统一,保证没有负号,只有加减,我在解析时候就可以安心保证,+ - 一定是项的分割,* 一定是因子的分割,后续不需要任何担忧,为什么不呢()
当然,预处理处理好,可以让后续解析非常安心方便。但有潜在的危险。首先是对于思维要求较清晰,想让预处理发挥极大作用,就必须有很高的清晰思路,去空白符,加减号合并,各种替换等等操作,有一个顺序颠倒都可能会导致结果出错,而且对于哪里可能有负号,都要考虑到,考虑不全也会挂。其次是时间增加,有人认为像这样加东西,会导致运行时间增加,但经过后续实验,这不会对运行时间造成什么可见影响。因为对于*(0-1),(0+,这样的情况,本来数据也可能出现,我当然优化过程的时候,要连其一起优化,写不写这个预处理,我都会去优化他,顺手的事。而且进行正常的优化后,每次强测运行时间都很快。
表达式解析
我采用的也是递归下降,根据Lexer和Parser类,按照训练中所提供的思路进行解析。加减号分割项,乘号分割因子即可。
数据储存
第一次作业中,我是用一个简单的HashMap来存每个类中的多项式数据,以x的指数和系数分别为key和value,好处是访问与计算,等等,都很方便,运行时间上很快速,缺点是可迭代性低,在第二次作业经历了表达式储存方面的重构。
表达式处理与输出
表达式,项,因子都建造BuildPoly方法,假设组成表达式的各个项的poly都已知,构建表达式的poly,这样同理往下写,直接调用一个顶层表达式的BulidPoly,就会自动递归建造所需表达式,然后进行表达式输出与化解即可。(化简等部分由于三次作业逐次增加,在下面统一说)
UML类图与结构分析
由于第一次作业的时候,代码结构还不大,所以直接放一整个类图即可。类图体现了整体的结构,所以结构分析和类图放在一起。
Lexer生成词根(token),Parser来解析词根,构建表达式树。
那表达式树是什么架构呢,expr为顶层,根据+-分割由一堆Term组成,一个Term根据*分割,由一堆因子组成,因子里,如果是数和x^b,这样,就到底了,如果是表达式因子(也就是遇到'('),那就重新解析表达式,注意,解析完,要再看有没有指数,有的话也储存进这个表达式因子。
表达式树种每一个类,都有一个poly(也就是一个HashMap储存的多项式),在这次作业,他不适合专门的类,所以不在显著的结构里,在树建造好之后,就可以构建多项式了。
代码规模
复杂度分析
类复杂度和方法复杂度以及上面的代码规模都很小,没有复杂爆红的地方,所以第一次作业不进行过多分析。也不放方法复杂度了(方法复杂度也都很低)
第二次作业
要求
新增自定义函数部分以及括号嵌套以及指数函数
基本思路
括号嵌套在第一次作业的架构中已经解决,无需考虑。
关于指数函数,导致第一次的简单的HashMap来储存数据便的困难,所以建立的更改后的unit和poly类,unit为基本单元,在这里就是 ,为了与第一次作业的预处理结合(会替换成(0+,而且为了便于统一,我更改为
,poly就是一个unit组成的Arraylist,也就是几个unit相加,组成数据储存部分。
自定义函数,使用了一个静态类,Definer来专门处理,遇到fgh,就把实参全取出,通过Definer来进行实参替换掉形参,然后替换后得到的字符串作为一个expr因子来重新解析。(注意替换时候小心exp的x被替换,刚替换进去的x被替换等等问题,危险负号先替换走,之后再替换回来即可),同时,由于自定义函数也有括号,所以在预处理部分把','替换为',0+'这样实参就看做expr即可,带入形参替换的时候,加一个扩号进去即可。
UML类图与结构分析
由于第二次作业开始,类图变得复杂,所以我先列出一个总的简单类图,在分别介绍两个重要的组成部分。从而便于观看与理清结构。
下面就是去除Poly和Unit的类图,可以看到,去除这两个之后,结构就清晰很多了。与第一次结构所变化的不大,只是在因子部分进行了增加ExpFunc因子和Func因子。
下面是这次重构所新增部分,也是我把类图分成这两个小类图的原因。已经介绍过,poly是多项式的集合,所以表达式树的每一个类都有poly,所以poly与其他每个类都有联系,这样将他们放在一起,会导致类图非常复杂,不便于观看。而即使不放在一起,结构也可以理清,因为这里的poly就是第一次架构的HashMap,只是单独储存了,并且其中是用Arraylist,而不是HashMap了,其中由于单独作为类,小心浅克隆导致问题,所以poly和unit里我重写了clone方法(因为poly里有一堆unit,unit里的exp里可能有poly,所以两个clone要互相调用,unit判等时候,也要这样进行才能确保完全相等(我对于里面的多项式判等,采用的是全解析,全相等,再用优化来解决全相等判断导致的时间复杂问题,而没有用字符串判等),当然,经过测试,也不影响运行时间,很快)
代码规模
代码规模变大,不过还可以接受。
复杂度分析
在下面的类图的复杂度分析里,我们可以看到有爆红成分,所以在这里复杂度不是可以略过的部分。主要是poly和unit复杂问题,因为我对于两者加了很多彻底的判等,clone,各种运算方法等,而且还有一些作用不大的方法没有及时删。
可以看到,方法里有如下几个爆红的部分,主要是poly的计算。这是由于我在进行poly运算的时候,加入了很多化简。计算中必须要随时注意,这一步是否需要开始进行化解,不然会导致后续产生对于一堆0+0+0+0与0+0+0+0乘这样简单的部分,我也会挨个乘,耽误时间,所以我在这里加入了很多化简,比如合并同类型,系数为0,就不放入,等方法。
这个爆红其实是可以很容易解决的,只要把化解的那么多相似性代码提出来,作为一个新的方法,这几个计算过程中调用他,即可,而在写的时候为了方便快速,我并没有提出来做出新的方法,只做了一个标记,待之后有时间的时候再进行提出(不过并没有很多时间)
第三次作业
要求
求导因子,自定义函数嵌套
基本思路
自定义函数嵌套根据第二次作业的方法,也已经解决。无需更改。
求导因子,新建一个类,作为导数因子类即可,在unit和poly等,构建相应的求导方法即可。(同样也是unit和poly互相包含时候求导,但由于第二次作业已有,unit和poly互相包含导致的clone,相等等判断的时候遇到过,所以难度不大)
UML类图与结构分析
主要形式同第二次一样,也是一个总体部分和两个主要部分。但是由于没有新增什么,只有一个DxFunc(导数函数因子)新增,所以没有太多要介绍的。
新增部分如下,多了个DxFunc
代码规模
复杂度分析
爆红的还是第二次作业出现的那些问题。
架构设计体验
架构的形成过程如上面三次作业的分析中写到。
关于重构方面,由于对于数据储存方面在第一次到第二次作业中进行了重构,导致对于表达式树中的各个类的数据的储存方式,各种加减乘等运算,都要改,所以大规模改变,虽然这部分内容,更改起来难度不大,但是量非常多,所以很麻烦。体验就是很糟心()
新的迭代场景的话,以加入sin因子为例。只需要在unit进行更改,然后对于计算进行更改,不需要进行大规模重构,就是增量开发即可。
bug与优化
未通过的公测用例和被互测发现的bug:无
在弱测和中测提交的时候,基本也都没出过什么bug(如果风格检测不算bug的话)。
自己在本地刚写完的时候,捏造数据进行测试的时候,发现过一点小bug,比如,如果结果是0,我输出的结果会是空,什么都不输出,所以对此进行了更改,如果表达式是空,那就是0。
对于这种bug也很好避免,因为在尝试捏造数据的时候,自己就想到,这方面好像没做到,就去尝试了。复杂度方面,没有什么严重的bug导致复杂度增加。
发现别人bug所采用的策略:三次互测中,所在房间都是全部0成功率,没法发现bug。尝试用复杂数据卡他TLE也做不到。至于根据代码设计结构,基本不可能把所有人代码看完,除非找到了谁的bug,或者心血来潮,才会把他的代码进行查看,从而定位错误位置。不过过程中发现,看和自己架构不一样的代码的时候,确实很花时间,所以很少做,而且通过这种方法找bug很难,代码太多了,定位bug还可能好一点。
优化方面,对于第一次作业优化到极致,比如,结果多余正负号,1*x^1,结果为x,这样,把每一个基本单元部分化到最简,,然后对于多个基本单元相加减,如果有正数,就把正数提前,避免多一个负号,长度加1。第二次作业开始,提公因式部分以及拆成多个exp相乘,并没有去做,其他部分都做了(只多优化了exp里面,要几层括号这种)。第三次作业也是。
优化方式,对于系数为1,x指数为0,为1,exp指数为0,这样的进行处理,特别注意,如果是1*x^0*exp(1)^0这样,按照简单的对于系数为1,x指数为0,就去掉这部分的方法,要注意,这里结果是1,而不是全去掉,变成空。正数提前,就看系数有没有正的就可以。exp里几层括号,就看他是不是个因子。
能保证正确性,简洁性也还可以。优化的时候,这几个优化是分别在不同方向上进行,互相干涉性没那么大,可以分别进行。而且,比如exp里几层括号,我单独写一个方法判断里面是不是因子,封装起来,就可以让简洁性大很多。
心得体会
第一次作业开始的时候,压力真的非常大,实在是无从下手。而且由于我在提前预想架构的时候,想到了很多可能会出现的问题,所以脑中形成的东西非常复杂,再加上不熟悉递归下降,所以又不会又想的多,更难形成具体架构。(思考的过多,也导致我跟别人讨论架构方向时候,虽然我对于具体如何实现还不了解,但我可能已经能指出他一些bug),好处是,想的更完善,不那么容易出问题,缺点也很严重,不熟悉的情况下,想的越细,越不容易理清,前期很崩。所以有时候,边写边理思路也是有用的。而且在互测看别人代码的时候,发现大家都用poly,mono什么的命名,很好奇这个名字是哪里的(我的poly是让翻译软件翻译“多项式”,得出的,我还以为大家都是这个翻译的),后来知道,都是一个师傅教的,所以第二次作业重构的时候,我也加入了()
而在设计方面,对于面向对象代码的书写,有了更好的掌握。而在设计架构时候,可拓展性的重要,也有一定的体会,不然重构很累,当然,经历重构,也帮助更好的分析自己的架构。而且,能为未来准备多少可拓展性,其实也取决于,目前时间还剩多少,对于快要完不成的人,他想准备更多拓展性也很难,所以要综合考量。
未来方向
这性能优化太容易出现一人刀全系的情况了,一人100分,其余都陪葬,建议平缓一点),实在有的优化策略太离谱了。
而且,第一次作业是从0到1,往往难度比想象的大,尤其对于各方面0基础的学生,往往要艰巨很多,所以希望尽量给更多,更平缓的指导。(虽然大部分人都会去找那相同的师傅),第三次作业确实难度很低,看完题目两小时就结束了,感觉这周没写oo一样(bushi),或许可以把第一次作业的一些难度后移到第三次。这样迭代更平滑,符合oo丝滑迭代的说法()。