BUAA 面向对象 第一单元 总结

”望涔阳兮极浦,横大江兮扬灵。“

BUAA 面向对象 第一单元 总结

前言

不要再为Unit1结束而悲伤了,马上到达的是Unit2,本次总结将从以下部分展开:

  • 程序结构分析与迭代架构

  • 程序测试方法及BUG分析

  • 优化策略

  • 反思与总结

    好,oo总结,启动!

程序结构分析与迭代架构

HW1

作业要求:

​ 读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号后的表达式。

基本思路:

​ 第一次作业本质上是对读入的表达式解析后运算。

解析方法笔者采用课程组推荐的递归下降法,如下图,其中Factor包含常数、幂函数(底数可以是任意因子)因子,表达式因子:
在这里插入图片描述
​ 即对读入的母表达式Expression,进行语法分析(Parser)后得到若干Term的Arraylist,然后对Term逐个Parser得到Factor的Arraylist。如果最后Factor作为表达式因子出现,则进行递归解析。虽然递归下降看起来比较复杂高深,一开始可能无从下手,但实际上手写一个ParserExpr(对表达式解析,笔者这里采取了do while的循环结构),便可以写出ParserTerm,然后对Factor进行分析,如此大体框架就开发好了。

运算方法在HW1中,仅仅涉及幂函数的加减乘三种运算,于是笔者采用了指数到系数的映射Hashmap<String, String> (此处为HW2的巨大重构埋下伏笔orz)。对解析后的母表达式进行解析后,运算便十分简单了,就是进行 ∑ i n T e r m [ i ] \sum_{i}^{n} Term[i] inTerm[i] 进行Key的比较,Value相加减,以及 ∏ i n F a c t o r [ i ] \prod_{i}^{n} Factor[i] inFactor[i]​​ 进行Key相加,Value相乘。

UML图:

在这里插入图片描述

类名称类设计思考优缺点分析
Factor作为因子父类这一父类在HW1中冗余(未有实例)
Const常数因子,继承自Factor字符串表示数字,完全可以使用BigInteger
Pow幂函数因子,继承自Factor,此时底数是传入底数的字符串表达式底数的数据类型需要再考虑,后续迭代十分困难
Expr表达式因子,继承自Factor,内有Term的容器/
Term项,内有Factor的容器/
Lexer词法分析器,可取得当前字符以及移动位置/
Parser语法分析器,处理递归下降以及因子识别/
Calculator进行多项式乘法、加法以及减法,获取最终输出,简化表达式考虑将输入、输出、计算分开,不应过度耦合
Main主类/
复杂度分析:

方法复杂度:

Methodev(G)基本复杂度iv(G)模块设计复杂度v(G)圈复杂度
Calculator.getAddExpression(String, String)4.06.07.0
Calculator.getDivided(String)1.05.05.0
Calculator.getoutput(HashMap)6.012.013.0
Calculator.merge(String[], HashMap)1.02.02.0
Calculator.multiple(String, String)1.07.07.0
Calculator.pairMul(String[], String[])1.01.01.0
Calculator.shorten(String)1.013.013.0
Calculator.simplify(String)1.06.06.0
Const.Const(BigInteger)1.01.01.0
Const.getExpression()1.01.01.0
Expr.addTerm(Term, boolean)1.01.01.0
Expr.Expr()1.01.01.0
Expr.getExpression()1.02.02.0
Factor.getExpression()1.01.01.0
Factor.getNegative()1.01.01.0
Factor.setNegative(boolean)1.01.01.0
Lexer.backward()1.01.02.0
Lexer.forward()1.01.02.0
Lexer.getConst()3.06.07.0
Lexer.getLength()1.01.01.0
Lexer.getPeek()2.02.02.0
Lexer.getPosition()1.01.01.0
Lexer.Lexer(String)1.01.01.0
Main.main(String[])1.01.01.0
Parser.Parser(Lexer)1.01.01.0
Parser.parserExpr()1.05.05.0
Parser.parserFactor()6.09.09.0
Parser.parserTerm()1.06.06.0
Pow.getExpression()3.04.05.0
Pow.Pow(String, BigInteger)1.01.01.0
Term.addFactor(Factor, boolean)1.01.01.0
Term.getExpression()1.03.04.0
Term.setNegative(boolean)1.01.01.0
Term.Term()1.01.01.0
Total52.0107.0114.0
Average1.52941176470588223.14705882352941173.3529411764705883

​ 注意到Calculator.getoutput(HashMap)Calculator.shorten(String)这两个方法的复杂度较高,这两个作为最后的输出和简化长度函数,需要整体遍历因子以及未处理的字符串,导致递归以及循环次数过多,这也是后续HW3、HW2导致TLE问题的关键原因。

代码规模:485行

HW2

作业要求:

​ 在HW1基础上,实现括号嵌套、exp()指数因子以及自定义函数

基本思路:

​ 在HW2中,需要加入对自定义函数的翻译以及对指数运算的支持

​ 对于自定义函数的翻译,笔者通过字符串解析,将变量x,y,z映射到因子,将自定义函数的表达式视作表达式因子,然后吸取递归下降的方法, 递归终点为”x“或者常数,此时只需要将x(指数因子)进行换底即可。

​ 但对指数运算的支持,由于在HW1中采用了Hashmap<String, String>这种简单粗暴的表达方式。导致在加入指数运算后,不得不重构。考虑到最终表达式为 [ a ∗ ] [ e e x p r e s s i o n ] [ ∗ x [ b ] [a*][e^{expression}][*x[^b] [a][eexpression][x[b]​​ 的形式,我最后采用了更加简单粗暴的方法:Hashmap<Factor,TrieMap<BigInteger, BigInteger>>将exp指数作为Hashmap的Key,然后将幂函数表达式作为Value,幂函数表达式同HW1一样处理。

​ 在对数据结构进行重构后,需要对乘法、加减法的运算进行重构和迭代,由于Factor实质上是指针,这导致Factor插入的时,如果不是以常数因子0(作为指数因子不存在的Key),必然作为全新的Key插入Hashmap(这导致了TLE的出现,在后续会讨论解决方法)。加减乘法就是两个Hashmap的合并问题,这个在HW1中以及实现过,即对Key进行加法即可。(在TLE了强测第2个点后,发现是在运算时候没有及时进行删“0”的操作,即TrieMap的Value为0时需要立即删除,在考虑到这个后,就不会TLE了)

UML图:

在这里插入图片描述

类名称类设计思考优缺点分析
Factor作为因子父类此时Factor作为’x’,‘y’,'z’的实例,但仍感觉可以和Pow合并
Const常数因子,继承自Factor/
Pow幂函数因子,继承自Factor,此时底数是传入底数的字符串表达式底数的类型现在只有Expr和Factor,常常因为底数是Expr使递归层数很高
EXtype指数因子,实际上可以视作另一形式的Pow与Pow一样,递归层数在计算时候很高,区别是一个为加法,一个为乘法。
Expr表达式因子,继承自Factor,内有Term的容器/
Term项,内有Factor的容器/
Lexer词法分析器,可取得当前字符以及移动位置/
Parser语法分析器,处理递归下降以及因子识别/
Calculator进行多项式乘法、加法以及减法采用的计算方法复杂度很高,影响运行时间
Input进行输入解析,输入简化Input与Output在缩短符号时候共用一个方法,或许该方法可以同时在这两个类中移除。
Output进行字符串输出,同类项合并,字符串缩短同上
Main主类/
复杂度分析:

方法复杂度:

MethodCog©ev(G)基本复杂度iv(G)模块设计复杂度
Calculator.Add(HashMap>, HashMap>)1.06.07.0
Calculator.del(HashMap>)1.04.04.0
Calculator.FactorAddFactor(Const, Const)1.01.01.0
Calculator.FactorAddFactor(Expr, Expr)1.03.03.0
Calculator.FactorAddFactor(Factor, Factor)5.09.09.0
Calculator.Mul(HashMap>, HashMap>)1.03.03.0
Calculator.TreeAddTree(HashMap, HashMap)1.04.04.0
Calculator.TreeMulTree(HashMap, HashMap)1.04.04.0
Const.add(Const, boolean)1.02.02.0
Const.Const(BigInteger)1.01.01.0
Const.getExpression()1.01.01.0
Const.getNumber()1.01.01.0
Const.getType()1.01.01.0
Expr.addTerm(Term, boolean)1.01.01.0
Expr.Expr()1.01.01.0
Expr.Expr(ArrayList)1.01.01.0
Expr.getExpression()1.02.02.0
Expr.getTerms()1.01.01.0
Expr.getType()1.01.01.0
Expr.mergeTerm()5.08.08.0
Expr.renew(HashMap)1.02.02.0
EXtype.EXtype(Pow)1.01.01.0
EXtype.getExpr()1.03.03.0
EXtype.getExpression()1.01.01.0
EXtype.getType()1.01.01.0
EXtype.isEquivalent(Factor)1.01.01.0
EXtype.renew(HashMap)1.01.01.0
Factor.add(Const, boolean)1.01.01.0
Factor.getEle()1.01.01.0
Factor.getExpression()1.02.02.0
Factor.getNegative()1.01.01.0
Factor.getType()1.01.01.0
Factor.isEquivalent(Factor)1.01.01.0
Factor.renew(HashMap)1.01.01.0
Factor.setEle(String)1.01.01.0
Factor.setNegative(boolean)1.01.01.0
Function.Function(ArrayList, ArrayList)1.02.02.0
Function.getExpr()1.01.01.0
Function.getExpression()1.01.01.0
Function.getType()1.01.01.0
Input.getExpression()1.01.01.0
Input.getFunction()1.02.02.0
Input.Input()1.01.01.0
Input.shorten(String)1.013.013.0
Lexer.backward()1.01.02.0
Lexer.forward()1.01.02.0
Lexer.getConst()3.06.07.0
Lexer.getLength()1.01.01.0
Lexer.getPeek()2.02.02.0
Lexer.getPosition()1.01.01.0
Lexer.Lexer(String)1.01.01.0
Main.main(String[])1.01.01.0
Output.Consolidate(String)1.01.01.0
Output.getExpExpression(String, String, HashMap, BigInteger)1.011.014.0
Output.getMonomial(String, HashMap, BigInteger)2.08.010.0
Output.getOutPut(HashMap>)1.07.08.0
Output.shorten(String)1.013.013.0
Parser.getConstFactor(boolean)1.01.01.0
Parser.getExpressionFactor()2.03.03.0
Parser.getEXtypeFactor()2.01.02.0
Parser.getFunctionFactor()5.06.06.0
Parser.getPowFactor(boolean)1.02.02.0
Parser.Parser(Lexer, HashMap)1.01.01.0
Parser.parserExpr()1.05.05.0
Parser.parserFactor()8.013.013.0
Parser.parserTerm()1.06.06.0
Pow.getBase()1.01.01.0
Pow.getExponent()1.01.01.0
Pow.getExpression()1.05.05.0
Pow.getType()1.01.01.0
Pow.isEquivalent(Factor)1.01.01.0
Pow.Pow(Factor, BigInteger)1.01.01.0
Pow.renew(HashMap)1.04.04.0
Term.addFactor(Factor, boolean)1.01.01.0
Term.getExpression()1.03.04.0
Term.getFactors()1.01.01.0
Term.getNegative()1.01.01.0
Term.isEquivalent(Term)5.02.05.0
Term.merge(Term)1.03.03.0
Term.renew(HashMap)1.02.02.0
Term.setNegative(boolean)1.01.01.0
Term.Term()1.01.01.0

​ 注意到在与获得输出相关的方法的复杂度较高,笔者分析,大概由于因为采用了解析表达式一致的递归下降方法,导致复杂度的急剧上升。这个也在HW3中导致TLE问题,现按下不表,会在HW3中分析解决方案。

代码规模:1008行

重构体验:

​ 这一次HW2由于HW1中采用了不太好的数据类型,主要问题是Pow的底数类型~~~(HW1中采用String,有点抽象)~~~、计算多项式的表达方式、将因子拆开采用正则的方法,导致HW1中近一半的代码均需要重构。但重构下来,对于面向对象的理解,确实有所提高。对于一些方法,感受到可以不必拘泥于小方法的细节,可以先实现大方法,形成框架,然后逐步往里面填“肉”。

HW3

作业要求:

​ 在HW2的基础上,实现求导算子,以及在自定义函数中实现定义时调用已有定义的函数。

基本思路:

对于求导算子,笔者的实现类似于自定义函数的实现,同样采用相似的递归下降方法。但不同的是,对于Term的求导,产生结果可能是一堆的Term(乘法求导导致),但无伤大雅。这个的实现十分简单,五十多行就可以解决。

​ 对于实现自定义函数的迭代,这个在HW2中已经实现了,这里考虑到在一些测试样例下导致递归层数十分高,需要对表达式解析进行优化。在调试中观察到在HW2中,为了避免解析错误,笔者直接将函数中的因子,全部抽象为表达式因子,这是导致递归层数增加的最主要原因。(如下图所示)
在这里插入图片描述
所以,采用parserFactor方法进行函数解析。但这导致了HW2中未发现的Bug暴露,直接原因是原先Pow类中的底数因子不是单个的x就是一个表达式(不论是常数还是指数因子,都会被视作一个表达式因子),但此时Pow类的底数可以是任意的因子。这直接导致程序在函数因子解析的时候无法正确换底,导致bug的出现~~(被hack的时候,发现这个问题还没被完全修复)~~。UML图与HW2一致,不再贴出。

复杂度分析:

方法复杂度分析:

Calculator.Add(HashMap>, HashMap>)9.01.06.07.0
Calculator.del(HashMap>)4.01.04.04.0
Calculator.FactorAddFactor(Const, Const)0.01.01.01.0
Calculator.FactorAddFactor(Expr, Expr)2.01.03.03.0
Calculator.FactorAddFactor(Factor, Factor)9.05.09.09.0
Calculator.Mul(HashMap>, HashMap>)3.01.03.03.0
Calculator.TreeAddTree(TreeMap, TreeMap)5.01.04.04.0
Calculator.TreeMulTree(TreeMap, TreeMap)7.01.04.04.0
Const.add(Const, boolean)2.01.02.02.0
Const.Const(BigInteger)0.01.01.01.0
Const.differentiate()0.01.01.01.0
Const.getExpression()0.01.01.01.0
Const.getNumber()0.01.01.01.0
Const.getType()0.01.01.01.0
Expr.addTerm(Term, boolean)0.01.01.01.0
Expr.differentiate()5.02.04.04.0
Expr.Expr()0.01.01.01.0
Expr.Expr(ArrayList)0.01.01.01.0
Expr.getExpression()1.01.02.02.0
Expr.getTerms()0.01.01.01.0
Expr.getType()0.01.01.01.0
Expr.mergeTerm()17.05.08.08.0
Expr.renew(HashMap)1.01.02.02.0
EXtype.differentiate()2.02.02.02.0
EXtype.EXtype(Pow)0.01.01.01.0
EXtype.getExpr()3.01.03.03.0
EXtype.getExpression()0.01.01.01.0
EXtype.getIn()0.01.01.01.0
EXtype.getType()0.01.01.01.0
EXtype.isEquivalent(Factor)0.01.01.01.0
EXtype.renew(HashMap)0.01.01.01.0
EXtype.setIn(Factor)0.01.01.01.0
Factor.add(Const, boolean)0.01.01.01.0
Factor.differentiate()0.01.01.01.0
Factor.getEle()0.01.01.01.0
Factor.getExpression()2.01.02.02.0
Factor.getNegative()0.01.01.01.0
Factor.getType()0.01.01.01.0
Factor.isEquivalent(Factor)0.01.01.01.0
Factor.renew(HashMap)0.01.01.01.0
Factor.setEle(String)0.01.01.01.0
Factor.setNegative(boolean)0.01.01.01.0
Function.Function(ArrayList, ArrayList, HashMap)1.01.02.02.0
Function.get()0.01.01.01.0
Function.getExpr()0.01.01.01.0
Function.getExpression()0.01.01.01.0
Function.getType()0.01.01.01.0
Input.getExpression()0.01.01.01.0
Input.getFunction()1.01.02.02.0
Input.Input()0.01.01.01.0
Input.shorten(String)18.01.013.013.0
Lexer.backward()1.01.01.02.0
Lexer.forward()1.01.01.02.0
Lexer.getConst()8.03.06.07.0
Lexer.getPeek()2.02.02.02.0
Lexer.Lexer(String)0.01.01.01.0
Main.main(String[])0.01.01.01.0
Output.Consolidate(String)0.01.01.01.0
Output.getExpExpression(String, String, TreeMap, BigInteger)16.01.08.010.0
Output.getMonomial(String, TreeMap, BigInteger)16.02.08.010.0
Output.getOutPut(HashMap>)18.01.07.08.0
Output.getString(String)4.01.05.06.0
Output.shorten(String)18.01.013.013.0
Parser.getConstFactor(boolean)0.01.01.01.0
Parser.getDifferentiation()0.01.01.01.0
Parser.getExpressionFactor()3.02.03.03.0
Parser.getEXtypeFactor()2.02.01.02.0
Parser.getFunctionFactor()10.05.06.06.0
Parser.getPowFactor(boolean)2.01.02.02.0
Parser.Parser(Lexer, HashMap)0.01.01.01.0
Parser.parserExpr()5.01.05.05.0
Parser.parserFactor()14.09.014.014.0
Parser.parserTerm()8.01.06.06.0
Pow.differentiate()7.04.04.05.0
Pow.getBase()0.01.01.01.0
Pow.getEle()0.01.01.01.0
Pow.getExponent()0.01.01.01.0
Pow.getExpression()11.01.05.05.0
Pow.getType()0.01.01.01.0
Pow.isEquivalent(Factor)0.01.01.01.0
Pow.Pow(Factor, BigInteger)0.01.01.01.0
Pow.renew(HashMap)5.01.06.06.0
Term.addFactor(Factor, boolean)0.01.01.01.0
Term.differentiate()10.01.05.05.0
Term.getExpression()4.01.03.04.0
Term.getFactors()0.01.01.01.0
Term.getNegative()0.01.01.01.0
Term.isEquivalent(Term)6.05.02.05.0
Term.merge(Term)5.01.03.03.0
Term.renew(HashMap)1.01.02.02.0
Term.setNegative(boolean)0.01.01.01.0

​ 仍然注意到,由于重复遍历元素,在关于表达式输出Output类中以及表达式优化的方法复杂度极高。这时常在exp嵌套层数高的时候,导致TLE,仍有极大的优化空间。

代码规模:1136行,可以看到改动相比于第一次迭代少了很多很多。

程序测试方法及BUG分析

程序测试方法

​ 在HW1中,感谢czx和Chatgpt的帮助从而生成一台自动评测机,在测试过程中,采用纯random的数据生成方式:

在这里插入图片描述

​ 在HW2和HW3中,感谢DPO OJ V2网站的建设同学,评测平台确实能够帮助发现一些Bug。除了借助该平台之外,笔者也针对自己的程序,通过手动构造递归层数较高的测试样例,从而降低总体运行时间,避免tle。

​ 在互测环节中,主要是结合本人在测试时发现的bug捏数据,比如HW1中捏0,HW2中exp的指数为0的情况进行hack。

BUG分析

自己的BUG

1.在HW2中,由于递归层数过高导致第二个点TLE,并且在互测中由于对表达式输出的理解错误,导致format error

2.在HW3中,对形如 f ( x ) = g ( x 2 ) f(x) = g(x^2) f(x)=g(x2) 的函数定义式,由于在优化运行时间是漏判(这很不oo),导致在互测中被刀了一次。

互测中他人的BUG

1.在HW1中,有同学因为对幂函数底数的解析有误,导致(x+0)^2会输出x^2+1

2.在HW2中,主要是exp的0次会出现一些bug

优化策略

​ 在三次作业中,优化主要分为两个方面:运行时间优化、输出长度优化

运行时间优化:

​ 对于笔者架构,其主要问题是递归层数过高导致TLE问题。显而易见,计算/输出一个表达式因子的递归深度远深于一个等价的其他类型因子,即对函数中因子、幂函数底数、指数进行精准分类,会大大降低运行时间。所以,在HW2到HW3中,针对HW2中无脑采用parserExpr将嵌套的因子一概而论地视作表达式因子,笔者进行了细化分类,从而大大降低运行时间。

输出长度优化:

​ 输出长度可以通过合并同类项、提取(最优的)公因子、符号调整等方面考虑。由于笔者实在愚钝,为了避免因为优化导致WA,所以仅进行了合并同类项,具体实现是根据我的输出表达方式: a ∗ e e x p r e s s i o n ∗ x b a*e^{expression}*x^b aeexpressionxb (如果a不存在,则对这个Term添加一个+1),从而实现Term的比较(逐个因子判断是否等价),但实际来看,由于expression中仍可能继续嵌套Term,此方法在部分情况下会失效,导致性能分极大丢失。

反思与总结

​ 虽然上过oopre,但HW1点开的一瞬间,才感觉到真正的oo并不是pre中的“造玩具”行为。在HW1中,因为没有良好的架构,导致HW2中大量的重构。虽然有些难绷,但确实在重构的时候,才感受面向对象实质上是一种宏观的模型,即一个类要实现能做什么,而不是用冗长的代码实现怎么做,要实现宏观布局,而不能局限于某一小方法。

​ 总体来说,oo第一单元的体验还是很好的,确实能够感受到自己编程思想上的转变。但是,对于代码性能分,我有一些自己的看法:个人认为可以如同五级制度一般进行分档,从而减少同学们(尤指我这样的懒人)在优化结果的内卷行为(比如将exp(2)提出来这种绝活哥掀起新的内卷浪潮)。

Unit 2 启动!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值