301
社区成员
发帖
与我相关
我的任务
分享




图中CogC代表认知复杂度,衡量代码是否容易理解;ev(G)衡量非结构化程度,ev(G)高意味着难以模块化和维护 ;iv(G)代表模块设计复杂度,用来衡量模块之间的调用关系;v(G) 代表圈复杂度,用来判定结构的复杂程度。
我的getString()方法比较复杂,原因是在处理输出时,我对系数为-1,0,1,指数为0和1的情况进行了特判,导致分支较多。

首先先用类Pre预处理字符串,然后用Lexer词法分析,Parser语法分析。分析后的语法树自顶向下为Expr,Term,Factor类,Factor一共有6种,用一个接口将他们统一管理起来,其中,Invoke类专门用来实现自定义函数的定义和调用。语法树的每个部分都有toPoly方法,转换为多项式poly。Poly类中用一个Hashmap管理单项式中的系数,变量指数和exp()里的poly,将变量指数和exp()里的poly封装成类Atom,将Atom和系数封装成Mono。最后用Output类对得到的Poly输出。
优点:建立语法树时用接口Factor统一管理,在新增因子时扩展性强;有专门的输入处理类Pre和输出处理类Output。
缺点:Mono类显得比较多余,这是代码多次迭代后的遗留产物。
利用递归下降的思想,建立一个表达式树。

从顶层向下层分别是Expr,Term,Factor。Expr是由Term相加得到的,Term是由Factor相乘得到的,而Factor又分为VarFactor,NumberFactor,ExprFactor,利用一个接口Factor对这三个类进行抽象管理。其中,ExprFactor这个类里面储存着括号里的表达式expr和指数exp。建立一个Lexer类,用一个move()方法实现在输入的字符串中遍历得到一个个的token。然后建立一个Parser类,对字符串自顶向下进行语法分析。首先实现解析Expr,若遇到"+",则将解析的Term加入Expr中的Arraylist<Term> terms。再实现解析Term,若遇到"*",则将解析的Factor加入Term中的Arraylist<Factor> factors。最后实现解析Factor,如果lexer返回的token是数字,则返回解析到的数字;如果是字母,则返回解析到的变量以及指数值(若没有指数则设指数为1);如果是括号,则先将括号里的expr解析出来,再解析指数的值,最后返回expr和指数结合起来的ExprFactor。
经过思考,发现任何表达式展开括号后都可能写成Σa*x^b(单项式相加)的形式。所以我新建了Poly类和Mono类,在Poly类中新建Arraylist<Mono> monos,在Mono类里存储系数a和指数b。在Expr,Term,Factor中新建toPoly()方法,将它们都转换为多项式。其中,Expr的toPoly是将terms中term的poly相加,Term中的toPoly是将factors中factor的poly相乘,ExprFactor是将expr的poly和指数做乘方运算。我在Poly类里实现了多项式之间的加法,乘法和乘方运算。在主函数中,我对解析后得到的expr调用toPoly()方法就能得到整个式子的多项式。最后新建一个Output类,实现getString方法,专门对输出做处理,并且对monolist为空(最后结果为0),mono中系数为0、1、-1,指数为0、1的情况进行了特判。
其中有四点需要注意:
第一点是合并同类项:我们必须保证在每一次poly的运算后都对其进行同类项合并,否则遇到诸如(1+x)^8的情况很容易超时。由于对迭代器的用法不熟,我在实现此功能时用了一个十分笨拙的方法。就是双重遍历monos容器,两个mono的指数相同时,则将前一个mono的系数增加一倍,后一个mono的系数置为0,最后在遍历一次将系数为0的mono移除。可以看到一共需要遍历至少三次,这也增加了时间复杂度,为我之后的代码重构埋下了伏笔。
第二点是对指数为0的情况的处理:在对poly进行乘方运算时,若出现指数为0的情况,则直接返回一个值为1的poly。
第三点是对"-"的处理:我的思路是把"-"看作一个numberfactor,再解析factor时遇到"-"就返回值为-1的numberfactor。这样做虽然思维难度简单,但很容易出现问题,因为有可能将"-"视为-1后,其前后没有"*"或者"+",我对此专门做出了预处理。
第四点是预处理:在进行词法分析前,我用了一个Pre类对字符串预处理。主要任务是将其中的空格符和\t去除,然后再将连续的符号化简为单个,然后将"*","^","("后的"+"处理掉,最后是将数字、变量、括号与在之前的"-"间添加"*",将数字、变量、反括号与在之后的"-"间添加"+"。这样做的目的是降低耦合度,实现单一职责原则。
可扩展性较差原因主要是在合并同类项时,我没有考虑到后续添加多种类型因子的情况。若添加exp()或者三角函数时,就不能简单地对指数进行相等判断,还要对exp()或三角函数里的expr是否相等进行判断。因此,Poly并不能简单地用Arraylist存储Mono,而应该用自带equals方法判断的hashmap。
第一次作业的优化主要是针对第一个单项式系数是负数的情况,将第一个系数是正数的单项式挪到开头即可解决。
第一次作业中未出现Bug

对指数因子:
由于指数因子exp(a)^b=exp(a*b),exp(a)*exp(b)=exp(a+b)的特性,任何多项式都可以写成Σa*x^b*exp(poly)(单项式相加)的形式。我的第一想法是在mono类里添加一个存放exp里的poly。我新建了一个ExpFactor类,里面存放括号里的factor以及指数exponent,在toPoly方法内,我新建一个mono,系数设为1,指数设为0,poly设为exp里的factor的poly乘exponent的poly,最后返回一个加入了该mono的poly;在实现多项式相乘时,对mono里的poly相加。
我主要是在合并同类项时遇到了问题。我原先的做法是将mono里面存放exp()里factor信息的poly先用getString方法得到展开后的字符串,然后在合并同类项时增加两个mono字符串相等的条件,但用这种方法的效率非常慢,遇到嵌套指数和exp()里的factor特别复杂的情况时,极有可能出现超时的bug。
经过思索后,我重构了代码。既然合并同类项要判断mono的指数和poly,所以我又新建了一个类Atom将mono的指数和poly封装起来,然后将Poly类里存放mono的容器arraylist改为hashmap<Atom, BigInteger>,这样做的目的是利用hashmap的put()和contains()方法,在addMono的时候会自动寻找与其atom相等的键值对,从而达到合并同类项的目的。只需要重写Mono, Atom, Poly的hashcode()和equals()方法,就可以比较轻易地实现。在addMono中,如果map的键中没有传入mono的atom,则put这个mono;若存在,则将找到的atom对应的系数与传入mono的系数相加,若结果为0,则remove掉这个键值对,否则更新系数。
对自定义函数因子:
新建了FuncFacter类,里面存放自定义函数调用实参后的字符串,以及该字符串解析后的表达式。新增Invoke类,用HashMap<String, String>存放函数名对应的表达式,HashMap<String, ArrayList<String>>存放函数名对应的形参,用静态addFunc()方法实现定义自定义函数时的功能,用静态invokeFunc()方法实现调用自定义函数的功能,通过实参替换形参实现。在parser解析到"f","g"或"h"时,继续解析的表达式,直到遇到")",将实参解析出来与函数名一并传入invokeFunc方法,得到代入实参后的函数字符串后就可以新建一个Funfactor并返回了。
在invokeFunc中,有几点要注意:
第一点:要先将自定义函数中的形参替换成"u","v","w"或其他变量,再将"u","v","w"替换成实参。否则会出现问题。例如对于f(x,y)=2*x-y,带展开的表达式为f(y,x),若直接先将"x"替换成"y",变为了2*y-y,再将"y"替换成"x",变为了2*x-x,最后结果为"x",显然不对。
第二点:要先将自定义函数中的"exp"替换成其他变量如"a",因为在替换时可能也会将"exp"中的"x"替换掉。
第三点:最好先将实参放进括号内将其当成一个整体再替换,否则也有可能出现问题。例如对于f(x)=2-x,带展开的表达式为f(-1),若实参"-1"不加括号,会替换为2--1,会出现问题,当然也可以再进行一遍预处理。
对负号:
考虑到将来有可能会加入多种因子,负号的位置可能会出现在各种意想不到的地方,所以我又进行了针对负号的重构。跟第一次作业中预处理负号不同,我在各种Factor的类中新增了sign属性,用来判断该Factor的正负情况,在Factor接口新增changeSign()方法,并在Poly类中新增changeSign()方法用来实现对多项式的取反。在parser中解析到"-"时就继续解析后面的factor,再调用changeSign并返回。
主要针对指数函数去掉一层括号,我首先无脑在exp()里再叠一层括号,然后再用replaceAll方法将里面是正负数或者变量的exp(())去掉一层括号。
在重写Atom的equals方法中,我在对BigInteger类型的指数exp判断相等时犯了错误,我直接用了相等判断符"==",但应该用equals判断。这导致我实现不了合并同类项的效果,在强测中几乎丧失了全部性能分,有一个点TLE,并且还在互测中被hack到了一个TLE的点。
0
(1+x+x^2+x^3)^6
由于经历了前两次作业中的重构,代码扩展性良好,本次作业不需要太多的变动,只需增加求导因子的实现。语法树变为:

新增DeFactor类,里面储存括号内的表达式。并且在Factor接口和Term,Expr中新增toDe()方法,返回对它们求导之后得到的Poly。根据求导法则具体实现:

其中,DeFactor类里面也要实现toDe()方法,因为可能会出现多层求导因子嵌套的情况,如dx(dx(……))。具体实现为:先将dx()括号内的expr解析为展开后的表达式字符串,再对这个字符串进行新的一轮词法和语法分析,将得到的表达式用toDe()得到求导后的poly后返回。
经过三次作业迭代后,我的代码扩展性良好。如果在将来新添对数因子,利用可以仿照指数因子,在Mono中新添Hashmap<Poly, BigInteger>用来存放对数的真数多项式和对应的指数,同时在Mono中实现addLog方法。但仍有不足之处,在自定义函数替换形参时,有可能用户的需求并不仅仅满足于有限的几个字母,这个时候不能简单地先将形参替换成"u","v","w",需要寻求更好的方法。
在第三次作业没有出现Bug
在第一单元中,我收获颇丰,我通过replaceAll方法巩固了一些正则表达式的知识,还第一次学会了($1)($2)在正则表达式中的用法;通过重写hashcode和equals方法,我加深了对hashmap用途和优点的理解;通过将预处理和处理输入单独封装成类,我学会了单一职责原则可以降低耦合;通过Factor接口抽象管理不同因子,我加深了对接口的理解。最重要的是,我学会了递归下降,自顶向下的思想。
希望每单元的训练提交后希望可以有个结果的反馈,这样能够更好掌握每单元的知识。