BUAA_OO第一单元总结

0000。。 学生 2024-03-23 11:20:36

UNIT1总结

hw1

hw1的要求是表达式括号展开,需要处理的因子有:常数,幂函数,表达式因子。

hw1架构分析

hw1架构图

img

大致工作流程如下:

  • Main读取输入的字符串input,字符串首先经过Preprocess模块处理,去掉多余的空格和连续的正负号,将简化后的字符串输入Lexer提取Token, 再将该包含输入信息的Lexer放入Parser。
  • 在Parser中根据文法字符串得到一个Expr, 该Expr由Terms组成,而Terms又由factors组成
  • 为了统一输出形式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 : 每个类中方法的总圈复杂度.

方法复杂度

methodCogCev(G)iv(G)v(G)
Expr.addTerm(Term)0.01.01.01.0
Expr.Expr()0.01.01.01.0
Expr.setNegative()0.01.01.01.0
Expr.toPoly()12.01.05.05.0
ExprFactor.ExprFactor(Expr, Integer, Boolean)0.01.01.01.0
ExprFactor.getExprExponent()0.01.01.01.0
ExprFactor.toPoly()1.01.02.02.0
Lexer.Lexer(String)18.011.010.012.0
Lexer.move()0.01.01.01.0
Lexer.notEnd()0.01.01.01.0
Lexer.now()0.01.01.01.0
Lexer.prev()2.02.02.02.0
Main.main(String[])0.01.01.01.0
Mono.getCoefficient()0.01.01.01.0
Mono.getExponent()0.01.01.01.0
Mono.Mono(Integer, BigInteger)0.01.01.01.0
Mono.Mono(Mono)0.01.01.01.0
Mono.toString()0.01.01.01.0
Num.Num(String)0.01.01.01.0
Num.toPoly()0.01.01.01.0
Num.toString()0.01.01.01.0
Parser.parsePow(Boolean)3.02.03.03.0
Parser.Parser(Lexer)0.01.01.01.0
Parser.parserExpr()8.01.07.07.0
Parser.parserFactor()10.05.08.09.0
Parser.parserTerm(boolean)2.01.03.03.0
Poly.addedMap(HashMap, HashMap)1.01.02.02.0
Poly.addmono(Mono)0.01.01.01.0
Poly.addPoly(Poly)0.01.01.01.0
Poly.getHashmap()0.01.01.01.0
Poly.getMonoList()0.01.01.01.0
Poly.mulPoly(Poly)0.01.01.01.0
Poly.multMap(HashMap, HashMap)3.01.03.03.0
Poly.negate()1.01.02.02.0
Poly.Poly()0.01.01.01.0
Poly.Poly(ArrayList, HashMap)2.01.03.03.0
Poly.Poly(Poly)2.01.03.03.0
Poly.powPoly(Poly, Integer)9.05.06.06.0
Poly.printMono(Integer, BigInteger)17.01.08.08.0
Poly.renewMonolist(HashMap)1.01.02.02.0
Poly.setExpCoe(HashMap)0.01.01.01.0
Poly.setPolyHash()7.01.04.04.0
Poly.toString()9.05.06.07.0
Power.getBase()0.01.01.01.0
Power.getExponent()0.01.01.01.0
Power.Power(String, String, Boolean)0.01.01.01.0
Power.toPoly()1.01.02.02.0
PreProcess.cleanDuplicateSign(String)16.01.06.07.0
PreProcess.cleanExponent(String)0.01.01.01.0
PreProcess.cleanFirstPlus(String)0.01.01.01.0
PreProcess.cleanWhite(String)0.01.01.01.0
PreProcess.Simplify(String)0.01.01.01.0
Term.addFactor(Factor)0.01.01.01.0
Term.Term()0.01.01.01.0
Term.Term(boolean)0.01.01.01.0
Term.toPoly()13.01.06.06.0
Token.getContent()0.01.01.01.0
Token.getType()0.01.01.01.0
Token.Token(Type, String)0.01.01.01.0
Token.toString()0.01.01.01.0
Total138.084.0132.0137.0
Average2.31.42.22.283333333333333

从图中看到,共有五个方法复杂度超标。

  • Lexer的构造方法接收字符串输入同时解析Token生成tokensList,其中包含了大量 if-else的条件判断,增加了复杂度。
  • Poly类中的powPoly方法对不同的指数进行了条件判断,增加了复杂度。
  • printMono和toString方法复杂度高的原因类似,都是因为要对不同情况的输出进行特判优化,因而复杂度高。而这也和我写代码时的感受一致。
  • cleanDuplicateSign的作用是清理多余的正负号,其中包含了嵌套,分支以及循环结构,因而复杂度高。

hw1架构设计体验

我的第一次作业代码架构是建立在训练及实验代码基础上的,我想先回顾一下我在实现架构中遇见的难点。

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));}

这样的架构在第一次作业中看似没什么问题,但是在接下来的第二次作业中就会给迭代带来比较大的困难。

hw1bug分析

本次作业在互测中被hack了四个点,原因都是相同的。原因是在尝试输出化简1时出了错,而这正是上述提到的复杂度偏高的printMono方法的职责。这恰恰说明了复杂度越高,就越容易出错,越需要大量测试。

1+(x^8)^8
x^641
// 错误在于,希望缩短表达式结果长度的时候将 +1 简化为 1但是忽略了1可能不在开头出现的情况。

hw2

hw2要求新增了指数函数因子,函数因子以及函数的调用。

hw2架构分析

hw2架构图

img

大致工作流程与hw1的类似,其中新增部分有:指数因子ExponentFactor,函数因子FuncFactor以及用于处理函数的工具类FuncManager。其中FuncManager的作用有:获得函数定义式,实参替换形参调用函数,将调用后的函数字符串解析为表达式。

复杂度分析

复杂度高的方法:

methodCogCev(G)iv(G)v(G)
Lexer.Lexer(String)21.014.013.015.0
Parser.parserFactor()12.07.010.011.0
Poly.multMap()28.028.01.01.013.013.014.014.0
Poly.powPoly(Poly, BigInteger)8.04.05.05.0
Poly.printMono(BigInteger, BigInteger, Poly)29.01.012.012.0
PreProcess.cleanDuplicateSign(String)16.01.06.07.0
Total114.028.059.064.0
  • hw2中的复杂度高的方法与hw1中的类似,并且新增了Parser.parserFactor()与Poly.multMap(HashMap>>, HashMap>>).
  • Parser.parserFactor()的复杂度高是由于因子种类增加导致的方法中判断语句增加。
  • Poly.multMap()复杂度高是由于没有对其中的方法进行封装,导致该方法包含了大量循环嵌套与判断语句。

hw2架构设计体验

对比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;

来表示一个多项式并进行多项式之间的运算,最终为此付出了及其惨痛的代价。

利用PolyHashMap进行Poly之间的运算实际上是把已经抽象化的Mono类又具体化成一种数据结构。这使得方法变得复杂,也提高了自己理解代码的难度。实际上Poly只需要monolists中的Mono便可以表示一个唯一的Poly, 一个单项式的指数、系数、以及exp因子内的多项式都是Mono类实例的成员,应在Mono类内处理。

在重构后方法的复杂度:

methodCogCev(G)iv(G)v(G)
Lexer.Lexer(String)21.014.013.015.0
Mono.equals(Object)6.04.03.07.0
Parser.parserFactor()12.07.010.011.0
Poly.addPoly(Poly)10.04.07.07.0
Poly.equals(Object)13.09.05.010.0
Poly.powPoly(Poly, BigInteger)8.04.05.05.0
Poly.printMono(BigInteger, BigInteger, Poly)31.01.013.013.0
PreProcess.cleanDuplicateSign(String)16.01.06.07.0
  • equals方法包含了大量条件判断语句以及通过循环来检验其成员的等价.
  • 原本进行乘法运算的方法multMap在重构后复杂度降低,这是因为对Mono的运算进行了封装。

hw2bug分析

hw2在重构前出现了几处bug:

  1. 进行加法运算时的深克隆问题导致的错误。

    在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的改变进而导致结果错误
    
  2. 函数实参替换形参时的字符串替换的问题。

    //函数调用时实参替换占位符
    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新增了求导因子以及对表达式的求导。

架构图

img

hw3相比与hw2新增了求导因子以及求导运算。因而每个因子需要实现求导方法takeDerivative().除此以外Expr,Term在语法层面也需要进行求导操作。

hw3架构分析

复杂度分析
methodCogCev(G)iv(G)v(G)
Lexer.Lexer(String)22.015.014.016.0
Mono.equals(Object)6.04.03.07.0
Mono.toString()15.01.011.011.0
Parser.parserFactor()14.08.012.013.0
Poly.addPoly(Poly)10.04.07.07.0
Poly.equals(Object)13.09.05.010.0
Poly.powPoly(Poly, BigInteger)8.04.05.05.0
Poly.printMono(BigInteger, BigInteger, Poly, int)56.01.021.021.0
PreProcess.cleanDuplicateSign(String)16.01.06.07.0
  • hw3中的复杂度高的方法与hw2中的类似,并且新增了两种复杂度高的Poly类和Mono类的equals方法,原因是equals方法包含了大量条件判断语句以及通过循环来检验其成员的等价.
  • Parser.parserFactor()的复杂度高是由于因子种类增加导致的方法中判断语句增加。
  • Poly.multMap()复杂度高是由于没有对其中的运算进行进一步封装,导致该方法包含了大量循环嵌套与判断语句,进而导致复杂度高。

hw3架构设计体验

hw3相比与hw2新增了求导因子以及求导运算。因而每个因子需要实现求导方法,例如常数求导,幂函数求导,指数函数求导等。在语法层面,Expr求导即是对其ArrayList<Term>中的每一个term求导,Term求导则需要使用乘法法则。

如果有一个比较优秀的架构,第三次作业的工作量相比于前两次作业来说会小很多。这也说明了一个优秀架构的重要性。

可能的新增需求

对于可能的新增需求,例如增加$sin,cos$因子.我的架构所需做可能有:

  1. 新建sinFactor以及cosFactor
  2. 在parseFactor中解析这两种三角函数因子,并在parser中定义解析方法。
  3. 单项式表示$ax^nexp(Poly)*sin(S)cos(C)$, 为了合并同类项需要重写equals方法

在这种情况下我的架构的可拓展性还是较高的,因为只需新增一些因子类并且补充对其的解析,运算方法即可。

hw3bug分析

hw3中强测以及互测都没有出现bug。

互测体验

  1. 使用程序曾经出现过的bug以及手捏数据测试
  2. 后面作业的互测稍微修改前面作业的强测或者互测数据后进行测试
  3. 在博客作业中学习了分析方法复杂度的方法,或许以后可以针对一些复杂度高的方法测试

关于优化

在优化表达式输出长度方面我只做了以下优化:

  • 合并同类项
    • 实现细节:在进行加法的时候对于$ax^nexp(Poly)$, 当指数相同以及Poly相同时合并同类项,这要求重写equals方法
  • 1*x, 1*exp(x)化简为x,exp(x). exp(0),x^0化为1.
    • 实现细节:依次考察$ax^nexp(Poly)$中a,n以及Poly的值在printMono中进行大量特判来优化。这个实现实际上并不简洁,printMono方法的复杂度也过高。或许可以进一步封装出分别针对系数,指数,Poly简化输出的方法。

在经过讨论区分享以及研讨课后,我学习到了更多的优化思路:

  • 提取公因式,做正优化
  • 保证exp内的括号不冗余

心得体会

回顾这三次作业,前两次作业给我带来的困难是最大的。在第一次作业前我用了很久的时间来理解递归下降算法、所给代码中的Lexer,Parser以及整体架构,因为从零理解一个全新的东西对我来说比较困难。在第二次作业中我花了比较久的时间纠结是否要重构,因为原有的代码难以实现多项式的化简。此外我还感受到:

  • 好好阅读课程规则,弱测有不通过的点记一次无效作业。要大幅提前ddl,因为总有意想不到的bug。
  • 要敢于重构,尤其是要敢于在作业过程中重构。因为在一个不好的复杂架构上频繁修改的工作量可能远比重构大。
  • 要多学习学长博客,多与同学交流沟通。

在写OO作业的过程中总有些时候会感到无助甚至无能为力,此时积极寻求帮助,多与同学交流是十分重要的。(甚至可能是让作业取得一些进展的唯一方法)十分十分感谢每一位帮助过我的老师、助教以及同学,每一个人的帮助对我来说都非常有意义。

未来方向

希望能在第一次作业时多讲解一些关于架构的内容,或者对训练,实验代码进行一定的讲解。

...全文
51 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧