前言
第一单元的主要任务是掌握递归下降方法,按照表达式的形式化表述对表达式的各结构进行分类建模,完成多括号的展开。并在之后的两次迭代任务中实现三角函数、自定义函数的解析以及对表达式求导的功能。本单元要体会层次化设计的思想。
1.第一次作业
第一次作业时因为对表达式各结构之间的联系不清楚,导致在多项式乘法处大量使用正则表达式做替换,对后续迭代极不友好,故在第二次作业时便进行了重构。
1.1 度量分析
1.1.1 代码规模分析
占主要行数的是Main和Merge文件,Merge为多项式的合并化简方法,因为第一次作业对结构不熟悉,导致没有在加入因子与加入项时就采取合并办法。同时由于toString()方法打印时盲目输出"+"号,使得在解析完表达式回到Main主函数进行结果输出时需要对最终字符串进行一系列化简以满足要求。
1.1.2 方法复杂度分析
cogC(cognitive complexity): 方法的认知复杂度是指方法的实现难度和理解难度。这包括方法中的控制流程、数据结构、算法和逻辑等方面。认知复杂度高的方法可能需要更多的时间和精力来理解和实现,同时也可能更容易出错。因此,在设计和编写方法时,需要考虑其认知复杂度,以便提高代码的可读性和可维护性。
ev(G) 基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。
iv(G) 模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。
v(G) 是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。
[原文链接:https://blog.csdn.net/Dkangel/article/details/106279052]
出现“红色”的merge和count方法是因为该类是对带指数的变量因子进行同类合并,而由于架构问题所以是通过遍历String字符串来实现合并,极其麻烦。同时mulPoly方法也是因为对两个多项式以String的方式相乘,使用了大量正则表达式,导致过于复杂。
1.1.3 类复杂度分析
OCavg(Average operation complexity): 平均操作复杂度
OCmax(Maximum operation complexity): 最大操作复杂度
WMC(Weighted method complexity): 加权方法复杂度
因为Merge和Polymulti类中有几个方法过于复杂,也导致整个类复杂度过高。所以要注意对方法的规模功能进行细化。
1.1.4 类图
分析:各类之间没有很好贯彻开放封闭原则(OCP),可扩展性不够强,单一类中过于臃肿,没有遵守单一职责原则(SRP)。
1.2 架构分析
依照training中的递归下降模式,但是整个解析过程只进行了去括号的过程。最终合并同类项以及符号化简都是在最终输出成字符串后对String处理。在项中获取因子时进行表达式因子乘法展开。因为大量采用正则表达式以及对字符串的处理,导致合并优化结果不好,且扩展性不佳,故在第二次作业时重构。
2.第二次作业
第二次作业新增了对三角函数与自定义函数的支持,同时在经过写完第一次作业后对整个结构有了真正的认识,故在第二次作业时进行了重构。
2.1 度量分析
2.1.1 代码规模分析
占行数多的类是Term(项)类、Expr(表达式)类、Parser(解析器)类。其中Term与Expr类中主要是toString()方法以及重写的equals()判断相等方法占了大量行数。而Parser则是在解析因子时因为因子种类增多而使得行数增多。
2.1.2 方法复杂度分析
出现“红色”的方法中,Expr和Term中判断两个表达式相等的equals方法因为我没有对各个项进行排序,导致判断每一项相等时都要重复遍历并标记是否相等,所以这是个问题。Term和Expr中toString()方法过于复杂则是因为其中我进行的因子判断多且每种因子的输出都不同。最简表达式相乘的mulPoly()方法负责是因为我没有单独进行Expr,幂函数Pow,三角函数Trigo等的深克隆方法,而是直接在一整个方法中克隆。parseFactor()与Lexer.next()均是因为其中else-if判断过多。
2.1.3 类复杂度分析
Lexer(分析器)中判取下一个终结符因为因子种类多而导致判断结构复杂。Term中则是判断相等(equals)以及toString太过冗杂而复杂。方法出现“红色”多的类也基本上是较为复杂的。
2.1.4 类图
首先整个表达式分三层:Expr -> Term -> Factor; Factor(因子)中又有四种因子{Pow(幂函数),Number(常数),Trigo(三角函数),Expr(表达式)}。通过Factor接口让各类因子同时实现接口。对于自定义函数和两个多项式相乘我单独写了两个类{Custom, Polymulti }来实现。
2.2 架构分析
大体上还是遵守Training中的架构,采用递归下降。
首先对于自定义函数的定义,由于最先输入,所以在Main主函数先对读入的自定义函数进行处理。在Custom类中对识别的形参按照顺序以“flag1,flag2,flag3”特殊字符串对其后的整个定义式中对应形参进行替换。当在后续解析到自定义函数因子时,只需把识别到的实参与对应“flag1,…”进行替换,然后再做解析来去括号。【注意因为多个实参中还可以是自定义函数,故直接以逗号分割会出错。我采取括号匹配的方法,在读到逗号时前面的括号也要全部匹配才算是一个实参】
在Parser类中分别对Expr,Term,Factor进行解析(parse);
Lexer类中则是读取下一个非终结符以及返回当前的读取符号。其中对于自定义函数和三角函数的读取,我设置读到“fgh”以及“sin,cos”后把里面的所有实参或者三角函数内表达式全部读完,并整体以String字符串的方式返回。
Expr中以arraylist存储Term(项);Term中以arraylist存储Factor(因子),以coefficient表示整个项的系数。【当识别到常数因子时会直接与系数相乘,故存储的因子中无常数因子】
在Lexer识别因子并返回给Term时,Term中的addFactor()方法会对因子与其中已有的因子进行相等比较(如变量x2与x3的比较合并)。
Pow,Trigo,Expr的指数均以BigInteger类型存储,各项系数也以BigInteger类型表示。
化简合并时就采用对每一项每一个因子进行比较。
3.第三次作业
第三次作业加入了求导运算以及在自定义函数定义中调用已定义函数的规则。迭代过程中只需实现求导运算,并在自定义函数的定义时单独进行处理即可。整体框架和架构保持不变且实现难度不高,重点是优化合并化简。
3.1 度量分析
3.1.1 代码规模分析
因为整体上迭代是在第二次作业基础上新增了求导运算类,和在自定义函数(Custom)类中进行处理,所以复杂度和规模与第二次作业类似。
3.1.2 方法复杂度分析
除了第二次作业留下的“顽疾”外,新增的“红色”是因为求导类(Derivation)中对表达式的整体求导过程比较复杂,因为考虑情况和判断比较多。
3.1.3 类复杂度分析
新增“红色”便是因为求导类的原因。
3.1.4 类图
在第二次作业的基础上新增了求导类(Derivation);同时在Term(项)类中因为要对整个项求导前先保证其为 *5xyzsin()cos()sin()*类似的形式,这样便于之后链式法则和乘法法则的求导 ,所以又添加了一些对因子按种类排序的方法。
3.2 架构分析
在第二次作业基础上迭代。先对Custom(自定义函数)类进行修改,只需在定义函数时先对函数表达式整体进行一次Lexer lexer = new Lexer(input); Parser parser = new Parser(lexer, customFun);
的解析来使其中的调用函数展开。这样还能对函数表达式中的求导进行处理。
另一项任务便是求导运算的编写。首先对整体表达式求导,即遍历每一个项,对每个项进行求导。对单个项先保证因子顺序为表达式因子最前,再递归求导表达式;之后保证变量在前,三角函数在后。此时整个项中除了系数,就只有幂函数因子和三角函数因子。
常数项求导为0,项中对幂函数求导再按照乘法法则对三角函数因子求导。不必提前判断整个项中是否有待求导变量,因为在整个求导过程中没有识别到待求导变量就自然求导为0. 在对三角函数因子求导的过程中因为其中有表达式,所以需要对整个表达式进行深克隆,来避免后续出错。
3.3 优化合并
对于合并同类项,在经过讨论课启发后发现,首先对单个项类的因子按某种特定顺序排序(如字典序),之后在比较两个项时直接遍历二者存储的因子。
对于三角函数的优化,比如sin((x-y))与 sin((y-x))
要考虑其中的表达式是否正好整体相差一个负号。
x**2 可以优化成 x*x
; 对sin(x)**2 + cos(x)**2 平方和的处理
;甚至考虑二倍角等。
对于长度的优化首先应确保自身的数据输出测试完全正确,注意不要在进行某些优化时意外导致比如少输出符号等BUG。
4.BUG分析
hw1
因为在对最终的字符串进行正则表达式化简时少考虑了几种情况,比如直接把(*1) 替换成 *
之类。也反映出自己第一次作业的化简和架构十分不可靠。
hw2
强测通过,互测因为sin(0)**0
和 sin((x-y)) - sin((y-x))
两个数据导致hack。实际上是自己在对三角函数因子做处理和化简时出错。
hw3
强测和互测均通过。
策略
头两次作业均是自己手动考虑某些情况,比如让所有因子都出现,专门调用特殊函数等进行判断,但自己仍会考虑不周。第三次作业采用了讨论区同学的评测机,在强测前发现了自己的一个BUG并进行了处理。评测机的搭建还需要自己尽快学习掌握。
在hack他人程序时,首先便是考虑满足尽可能复杂的程序,同时对于0,超大数,三角函数嵌套,自定义函数实参形参位置
等情况格外重视。
5.心得体会
①三次作业进行下来尤其经过一次重构,一次迭代。对我启示便是在写程序前首先对整个架构,整个框架,整个处理过程自己在脑中想清楚并画在纸上。考虑用几个类,分别实现什么功能,类的属性是什么,用什么类型存储,继承和实现怎么弄。之后进行的便是将整个过程“翻译”成程序。
②注意具体写程序的过程中对于自己想实现的功能是否有更好或更易拓展的写法,我要选择哪些容器或使用java的哪些类。
③注意动态数组遍历的越界导致nullPointerException
;注意深克隆问题
;
④自己的测试数据很可能有纰漏,依赖评测机的数据生成与比对。
⑤debug时要自己尝试去想办法输入可能出错的数据,并一步步调查,通常都是几行的代码问题导致整个出错。
我对递归下降以及正则表达式有了一定的掌握,同时对Java程序编写过程中常用的类、容器等有了基础。后续还要掌握设计模式来进行更好的架构以及对Java编写和面向对象思想进行进一步地学习。