301
社区成员
发帖
与我相关
我的任务
分享hw1的要求是表达式括号展开,需要处理的因子有:常数,幂函数,表达式因子。
hw1架构图

大致工作流程如下:
input,字符串首先经过Preprocess模块处理,去掉多余的空格和连续的正负号,将简化后的字符串输入Lexer提取Token, 再将该包含输入信息的Lexer放入Parser。coe*x^exoponent, 使用一个Mono类对形式进行统一Mono组成的多项式Poly,而我实现的方式实际上是把每一个Factor转换成只含有一个Mono的Poly类,例如Num就是一个exponent==0的Mono。最终通过Poly之间的运算得到最终的ansPoly。完成时,也就是说我们大致需要实现3个模块,它们的作用分别是:输入解析,将解析结果转化为可运算的形式,计算并输出。
复杂度分析所使用的是IDEA中的MetricsReloaded插件,只需要在Settings搜索Plugin然后在Marketplace搜索并安装即可。
对于各项指标的含义,我在学长的博客上找到了回答:
摘自https://www.cnblogs.com/yangxiaohan-aka/p/16041442.html
Method
- Cogc : 认知复杂度,其目的是显式地度量可理解性,随着每个控制结构的使用而增加,而且嵌套控制结构越多,认知复杂度就越高。
- ev(G) : 本质复杂度,是一种图论度量方法控制流的结构不良程度的方法。
- iv(G) : 设计复杂度,与方法控制流与对其他方法的调用之间如何相互关联有关。
- v(G) : 圈复杂度,是对通过每个方法的不同执行路径数量的度量,也可以被认为是完全执行方法控制流所需的最小测试数。
Class
- OCavg : 每个类中所有非抽象方法的平均圈复杂度(继承的方法不计算在内)。
- OCmax : 每个类中非抽象方法的最大圈复杂度(继承的方法不计算在内)。
- WMC : 每个类中方法的总圈复杂度.
方法复杂度
| method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Expr.addTerm(Term) | 0.0 | 1.0 | 1.0 | 1.0 |
| Expr.Expr() | 0.0 | 1.0 | 1.0 | 1.0 |
| Expr.setNegative() | 0.0 | 1.0 | 1.0 | 1.0 |
| Expr.toPoly() | 12.0 | 1.0 | 5.0 | 5.0 |
| ExprFactor.ExprFactor(Expr, Integer, Boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
| ExprFactor.getExprExponent() | 0.0 | 1.0 | 1.0 | 1.0 |
| ExprFactor.toPoly() | 1.0 | 1.0 | 2.0 | 2.0 |
| Lexer.Lexer(String) | 18.0 | 11.0 | 10.0 | 12.0 |
| Lexer.move() | 0.0 | 1.0 | 1.0 | 1.0 |
| Lexer.notEnd() | 0.0 | 1.0 | 1.0 | 1.0 |
| Lexer.now() | 0.0 | 1.0 | 1.0 | 1.0 |
| Lexer.prev() | 2.0 | 2.0 | 2.0 | 2.0 |
| Main.main(String[]) | 0.0 | 1.0 | 1.0 | 1.0 |
| Mono.getCoefficient() | 0.0 | 1.0 | 1.0 | 1.0 |
| Mono.getExponent() | 0.0 | 1.0 | 1.0 | 1.0 |
| Mono.Mono(Integer, BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
| Mono.Mono(Mono) | 0.0 | 1.0 | 1.0 | 1.0 |
| Mono.toString() | 0.0 | 1.0 | 1.0 | 1.0 |
| Num.Num(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| Num.toPoly() | 0.0 | 1.0 | 1.0 | 1.0 |
| Num.toString() | 0.0 | 1.0 | 1.0 | 1.0 |
| Parser.parsePow(Boolean) | 3.0 | 2.0 | 3.0 | 3.0 |
| Parser.Parser(Lexer) | 0.0 | 1.0 | 1.0 | 1.0 |
| Parser.parserExpr() | 8.0 | 1.0 | 7.0 | 7.0 |
| Parser.parserFactor() | 10.0 | 5.0 | 8.0 | 9.0 |
| Parser.parserTerm(boolean) | 2.0 | 1.0 | 3.0 | 3.0 |
| Poly.addedMap(HashMap, HashMap) | 1.0 | 1.0 | 2.0 | 2.0 |
| Poly.addmono(Mono) | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.addPoly(Poly) | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.getHashmap() | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.getMonoList() | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.mulPoly(Poly) | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.multMap(HashMap, HashMap) | 3.0 | 1.0 | 3.0 | 3.0 |
| Poly.negate() | 1.0 | 1.0 | 2.0 | 2.0 |
| Poly.Poly() | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.Poly(ArrayList, HashMap) | 2.0 | 1.0 | 3.0 | 3.0 |
| Poly.Poly(Poly) | 2.0 | 1.0 | 3.0 | 3.0 |
| Poly.powPoly(Poly, Integer) | 9.0 | 5.0 | 6.0 | 6.0 |
| Poly.printMono(Integer, BigInteger) | 17.0 | 1.0 | 8.0 | 8.0 |
| Poly.renewMonolist(HashMap) | 1.0 | 1.0 | 2.0 | 2.0 |
| Poly.setExpCoe(HashMap) | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.setPolyHash() | 7.0 | 1.0 | 4.0 | 4.0 |
| Poly.toString() | 9.0 | 5.0 | 6.0 | 7.0 |
| Power.getBase() | 0.0 | 1.0 | 1.0 | 1.0 |
| Power.getExponent() | 0.0 | 1.0 | 1.0 | 1.0 |
| Power.Power(String, String, Boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
| Power.toPoly() | 1.0 | 1.0 | 2.0 | 2.0 |
| PreProcess.cleanDuplicateSign(String) | 16.0 | 1.0 | 6.0 | 7.0 |
| PreProcess.cleanExponent(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| PreProcess.cleanFirstPlus(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| PreProcess.cleanWhite(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| PreProcess.Simplify(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| Term.addFactor(Factor) | 0.0 | 1.0 | 1.0 | 1.0 |
| Term.Term() | 0.0 | 1.0 | 1.0 | 1.0 |
| Term.Term(boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
| Term.toPoly() | 13.0 | 1.0 | 6.0 | 6.0 |
| Token.getContent() | 0.0 | 1.0 | 1.0 | 1.0 |
| Token.getType() | 0.0 | 1.0 | 1.0 | 1.0 |
| Token.Token(Type, String) | 0.0 | 1.0 | 1.0 | 1.0 |
| Token.toString() | 0.0 | 1.0 | 1.0 | 1.0 |
| Total | 138.0 | 84.0 | 132.0 | 137.0 |
| Average | 2.3 | 1.4 | 2.2 | 2.283333333333333 |
从图中看到,共有五个方法复杂度超标。
Token生成tokensList,其中包含了大量 if-else的条件判断,增加了复杂度。我的第一次作业代码架构是建立在训练及实验代码基础上的,我想先回顾一下我在实现架构中遇见的难点。
Lexer Parser
为了实现课题组推荐的递归下降算法,我们引入了两个概念:Lexer和Parser。其中Lexer用于解析词法,而Parser用于根据表达式的形式化定义解析语法。一开始在不太理解Lexer与Parser的概念的时候我并没有太读懂training-advance中给的代码,因为其中的Lexer只是接受了字符串输入以及定义了一些方法,而其调用则是在Parser中与语法分析同时进行的。这种"同时"分析词法和语法的实现让刚接触hw1的我感到有些疑惑,因此我的Lexer采取的是十分朴素的方法,即先遍历字符串分析出每个Token,将其存入tokensList中,再在分析语法的过程中在Parser里遍历tokensList.
理解了Lexer后,Parser所需的工作只是严格遵循表达式的形式化表述对表达式进行解析,这在训练代码中已经给出大致写法。我们所要做的实际上是根据不同的因子补充parseFactor的部分。
架构的问题
该架构最大的问题在Poly类中,由于Poly不可避免地需要进行运算,例如(x+1)^2,那么运算是否简便就很大程度上决定了架构是否轻便。
而我的Poly类的实现则非常复杂。为了进行运算以及化简,我很自然地想到了使用HashMap进行运算,因为HashMap在本次作业中合并同类项时有着天然的优势和简便,这使得实现本次作业较为简单,但却为下次作业挖下了大坑。
在本次作业的架构中我的运算实际上是通过private HashMap<Integer, BigInteger> expCoe来进行的,在进行addPoly时我实际上是把两个Poly各自表示指数_系数的HashMap进行合并:
ans.putAll(thismap);
//...
ans.merge(exp, othermap.get(exp), (oldvalue, newvalue) -> oldvalue.add(newvalue));}
这样的架构在第一次作业中看似没什么问题,但是在接下来的第二次作业中就会给迭代带来比较大的困难。
本次作业在互测中被hack了四个点,原因都是相同的。原因是在尝试输出化简1时出了错,而这正是上述提到的复杂度偏高的printMono方法的职责。这恰恰说明了复杂度越高,就越容易出错,越需要大量测试。
1+(x^8)^8
x^641
// 错误在于,希望缩短表达式结果长度的时候将 +1 简化为 1但是忽略了1可能不在开头出现的情况。
hw2要求新增了指数函数因子,函数因子以及函数的调用。
hw2架构图

大致工作流程与hw1的类似,其中新增部分有:指数因子ExponentFactor,函数因子FuncFactor以及用于处理函数的工具类FuncManager。其中FuncManager的作用有:获得函数定义式,实参替换形参调用函数,将调用后的函数字符串解析为表达式。
复杂度高的方法:
| method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Lexer.Lexer(String) | 21.0 | 14.0 | 13.0 | 15.0 |
| Parser.parserFactor() | 12.0 | 7.0 | 10.0 | 11.0 |
| Poly.multMap() | 28.028.0 | 1.01.0 | 13.013.0 | 14.014.0 |
| Poly.powPoly(Poly, BigInteger) | 8.0 | 4.0 | 5.0 | 5.0 |
| Poly.printMono(BigInteger, BigInteger, Poly) | 29.0 | 1.0 | 12.0 | 12.0 |
| PreProcess.cleanDuplicateSign(String) | 16.0 | 1.0 | 6.0 | 7.0 |
| Total | 114.0 | 28.0 | 59.0 | 64.0 |
Parser.parserFactor()的复杂度高是由于因子种类增加导致的方法中判断语句增加。Poly.multMap()复杂度高是由于没有对其中的方法进行封装,导致该方法包含了大量循环嵌套与判断语句。对比hw1,hw2最大的不同是它需要支持函数的定义以及调用,同时还需要支持嵌套括号。其中嵌套括号由于递归下降算法的特性已在hw1中解决。
函数的定义
启发自学长的博客,我使用了3个HashMap来描述一个函数。
HashMap<String, String> funcBody = new HashMap<>();// 函数体字符串
HashMap<String, ArrayList<String>> funcParas = new HashMap<>(); // 函数形参列表
HashMap<String, Expr> funcBodyExpr = new HashMap<>(); // 函数体Expr
函数的调用
调用函数时,先将对应位置的形参用占位符替换,再将函数体转化为字符串替换掉占位符,得到调用后的函数表达式字符串,再对其解析就可以得到Expr类型的表达式。
函数因子
// FuncFactor.java
private String calledFuncString;
private Expr funcExpr;
public FuncFactor(String name, ArrayList<Factor> actualParas) {
this.calledFuncString = FuncManager.callFunc(name, actualParas);
this.funcExpr = FuncManager.StringtoExpr(calledFuncString);
}
解析函数因子时,只需解析实参列表,然后将实参列表传入函数调用方法得到调用后的函数字符串,再对该字符串进行解析得到可运算的Expr. 实际上我们解析FuncFactor时期望得到的是Expr类型的funcExpr
hw2经历了重构,在hw1中,为了进行运算以及化简,我很自然地想到了使用HashMap来进行Poly间的运算:
//hw1 Poly.java
private ArrayList<Mono> monoList;
private HashMap<Integer, BigInteger> expCoe;
于是在一开始做hw2的时候,我便刻舟求剑地使用了类似地方法,尝试用:
//hw2 Poly.java
private ArrayList<Mono> monoList;
private HashMap<BigInteger, ArrayList<Pair<BigInteger, Poly>>> expCoePoly;
来表示一个多项式并进行多项式之间的运算,最终为此付出了及其惨痛的代价。
利用Poly的HashMap进行Poly之间的运算实际上是把已经抽象化的Mono类又具体化成一种数据结构。这使得方法变得复杂,也提高了自己理解代码的难度。实际上Poly只需要monolists中的Mono便可以表示一个唯一的Poly, 一个单项式的指数、系数、以及exp因子内的多项式都是Mono类实例的成员,应在Mono类内处理。
在重构后方法的复杂度:
| method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Lexer.Lexer(String) | 21.0 | 14.0 | 13.0 | 15.0 |
| Mono.equals(Object) | 6.0 | 4.0 | 3.0 | 7.0 |
| Parser.parserFactor() | 12.0 | 7.0 | 10.0 | 11.0 |
| Poly.addPoly(Poly) | 10.0 | 4.0 | 7.0 | 7.0 |
| Poly.equals(Object) | 13.0 | 9.0 | 5.0 | 10.0 |
| Poly.powPoly(Poly, BigInteger) | 8.0 | 4.0 | 5.0 | 5.0 |
| Poly.printMono(BigInteger, BigInteger, Poly) | 31.0 | 1.0 | 13.0 | 13.0 |
| PreProcess.cleanDuplicateSign(String) | 16.0 | 1.0 | 6.0 | 7.0 |
hw2在重构前出现了几处bug:
进行加法运算时的深克隆问题导致的错误。
在addPoly方法中调用了public HashMap<BigInteger, ArrayList<Pair<BigInteger, Poly>>> addedMap(
HashMap<BigInteger, ArrayList<Pair<BigInteger, Poly>>> thismap,
HashMap<BigInteger, ArrayList<Pair<BigInteger, Poly>>> othermap)方法
通过ans存储两个Poly加法运算后得到的HashMap进而得到Poly
HashMap<BigInteger, ArrayList<Pair<BigInteger, Poly>>> ans = new HashMap<>();
ans.putAll(thismap);//实际上是浅拷贝
这导致两个多项式分别遍历单项式mono相加时,ans的改变会导致其中一个Poly的改变进而导致结果错误。
函数实参替换形参时的字符串替换的问题。
//函数调用时实参替换占位符
if (...) {
calledFuncString = calledFuncString.replace("@", actualParas.get(0).toString());
}
//应该为:
if (...) {
calledFuncString = calledFuncString.replace("@", actualParas.get(0).toPoly().toString());
}
这实际上是混淆了Expr与Poly的toString()方法,Expr的toString()方法在我的实现中只是用于调试,而运算时实际上调用的应该是Poly的toString方法。
hw3新增了求导因子以及对表达式的求导。
架构图

hw3相比与hw2新增了求导因子以及求导运算。因而每个因子需要实现求导方法takeDerivative().除此以外Expr,Term在语法层面也需要进行求导操作。
| method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Lexer.Lexer(String) | 22.0 | 15.0 | 14.0 | 16.0 |
| Mono.equals(Object) | 6.0 | 4.0 | 3.0 | 7.0 |
| Mono.toString() | 15.0 | 1.0 | 11.0 | 11.0 |
| Parser.parserFactor() | 14.0 | 8.0 | 12.0 | 13.0 |
| Poly.addPoly(Poly) | 10.0 | 4.0 | 7.0 | 7.0 |
| Poly.equals(Object) | 13.0 | 9.0 | 5.0 | 10.0 |
| Poly.powPoly(Poly, BigInteger) | 8.0 | 4.0 | 5.0 | 5.0 |
| Poly.printMono(BigInteger, BigInteger, Poly, int) | 56.0 | 1.0 | 21.0 | 21.0 |
| PreProcess.cleanDuplicateSign(String) | 16.0 | 1.0 | 6.0 | 7.0 |
Parser.parserFactor()的复杂度高是由于因子种类增加导致的方法中判断语句增加。Poly.multMap()复杂度高是由于没有对其中的运算进行进一步封装,导致该方法包含了大量循环嵌套与判断语句,进而导致复杂度高。hw3相比与hw2新增了求导因子以及求导运算。因而每个因子需要实现求导方法,例如常数求导,幂函数求导,指数函数求导等。在语法层面,Expr求导即是对其ArrayList<Term>中的每一个term求导,Term求导则需要使用乘法法则。
如果有一个比较优秀的架构,第三次作业的工作量相比于前两次作业来说会小很多。这也说明了一个优秀架构的重要性。
可能的新增需求
对于可能的新增需求,例如增加$sin,cos$因子.我的架构所需做可能有:
sinFactor以及cosFactor类在这种情况下我的架构的可拓展性还是较高的,因为只需新增一些因子类并且补充对其的解析,运算方法即可。
hw3中强测以及互测都没有出现bug。
在优化表达式输出长度方面我只做了以下优化:
printMono中进行大量特判来优化。这个实现实际上并不简洁,printMono方法的复杂度也过高。或许可以进一步封装出分别针对系数,指数,Poly简化输出的方法。在经过讨论区分享以及研讨课后,我学习到了更多的优化思路:
回顾这三次作业,前两次作业给我带来的困难是最大的。在第一次作业前我用了很久的时间来理解递归下降算法、所给代码中的Lexer,Parser以及整体架构,因为从零理解一个全新的东西对我来说比较困难。在第二次作业中我花了比较久的时间纠结是否要重构,因为原有的代码难以实现多项式的化简。此外我还感受到:
在写OO作业的过程中总有些时候会感到无助甚至无能为力,此时积极寻求帮助,多与同学交流是十分重要的。(甚至可能是让作业取得一些进展的唯一方法)十分十分感谢每一位帮助过我的老师、助教以及同学,每一个人的帮助对我来说都非常有意义。
希望能在第一次作业时多讲解一些关于架构的内容,或者对训练,实验代码进行一定的讲解。