文章目录
BUAA OO 2024Unit1总结
00前言
写在前面
记得上学期上完OOpre之后,我曾说很期待与OO的正式会面,希望OO正课对我“手下留情”。然后就是冬天来了又去,在春暖花开之际,我与面向对象程序设计这门课正式相见。本文是我对BUAA面向对象第一单元的编程总结,既是对自己的总结,也希望能对未来学习面向对象程序设计这门课的同学给予帮助。
概述
由于本课程每一单元任务采取迭代开发的形式,因此我将按照时间发展顺序,依次介绍自己三次迭代开发的类图分析、架构设计、优化策略、bug分析等。由于本人水平不足尝试搭建可靠的测评机失败,于是本文没有测评机搭建经验分享,希望未来的我有能力实现测评机的搭建。
第一单元任务主要是表达式去括号化简,即读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号后的表达式。在后续开发中加入了指数函数、自定义函数和求导因子等。
你需要有一定基础的
Java
语言基本语法知识- 梯度下降算法
SOLID
设计原则- 复杂度分析工具
Metrics
01第一次作业
类图分析
先上类图
架构设计
设计思路
第一次作业中,表达式只含有加、减、乘、乘方、括号以及唯一的变量x。我在设计时将步骤分为解析、构建、输出两部分:解析的目的是拆解表达式结构,构建表达式树;构建的目的是将表达式树构建成去括号后的表达式;输出的目的是将去括号的表达式化简并输出。
解析这一部分采用了递归下降算法。具体做法如下:
-
首先,创建
Token
类,表示最小的语法单位,可以是数字、变量x
,也可以是+
、-
、*
、^
、括号等符号。再创建Lexer
类将输入的字符串分析成一个个Token
,形成一个Token
流。Lexer
将保存Token
流,并且可以通过next()
方法从头遍历Token
流。Lexer
分析过程中实现了去空格、连续加减号化简等预处理工作,此处的处理一定格外小心,否则可能会对解析产生影响进而埋下bug。public class Lexer { private final ArrayList<Token> tokens = new ArrayList<>(); private int curpos; public Lexer(String input) { int pos = 0; while (pos < input.length()) { /*将字符串处理成一个Token流*/ } } public void next() { curPos++;} }
-
接下来就是解析的核心部分,即对题目要求的表达式进行语法分析。不难看出题目要求的表达式可以解析如下形式化表达:
表达式 = 项 +/- 项 +/-……
项 = 因子 * 因子 * ……
所以我们要做的就是对一个表达式拆分成一个个项,再将一个个项拆分成一个个因子。于是首先构建
Expr
、Term
两个类分别表示表达式和项,以及一个接口Factor
表示因子。这里使用接口的原因是因为因子的种类很多并且存在许多同名方法,并且具备很好的可扩展性,重要的是可以很好的通过把Expr
接入Factor
接口来实现解析表达式因子。因子还可能是带符号整数或者变量x,因此构建对应的类Number
和Var
并接入Factor
。在细节上,我考虑了表达式内部的项之间的运算可以是加法也可以是减法,因此我的
Expr
类内部成员除了terms
还有ops
。同时考虑到Expr
作为表达式因子的时候可能会带有乘方,因此我还加入了exp
变量记录指数。 -
接下来构建一个
Parser
类表示解析器,用来完成拆解分析Token
流的工作。我在
Parser
类中实现三个方法parseExpr
、parserTerm
、paserFactor
,分别用于解析表达式、项、因子。我在parseExpr
中调用parserTerm
方法,在parserTerm
中调用paserFactor
方法,实现了递归调用并最终构建了一个表达式树。当解析到Number
和Var
时递归到终点。注意!当我们解析因子时,会发现表达式也是一种因子。于是乎完整的表达式解析应该是这样的。
至于
Parser
具体方法实现就很简单了。伪代码如下:Expr parseExpr() { ArrayList<Term> terms = new ArrayList<>(); ArrayList<Token> ops = new ArrayList<>(); terms.add(parserTerm()); while (/*当前Token是'+'或者'-'*/)) { ops.add(/*'+'或'-'*/); /*遍历下一个Token*/ terms.add(parserTerm()); } /*如果表达式有乘方,记录一下指数exp*/ /*返回一个Expr*/; }
parserTerm
的写法跟parserExpr
类似,只不过我在解析第一个因子之前判断了一下是否是-
:如果是减号就把-1
加入该项;然后继续调用parserFactor
。这里的处理主要是应对第一项可能会出现的-
号。Term parserTerm() { ArrayList<Factor> factors = new ArrayList<>(); if (/*如果是减号*/) { factors.add(new Number("-1")); /*跳到下一个Token*/ } factors.add(paserFactor()); while (/*如果是乘号*/)) { /*遍历下一个Token*/ factors.add(paserFactor()); } /*返回一个Term*/; }
parserFactor
则需要分类讨论因子究竟是带符号数字
还是单变量x
还是表达式因子
。Factor paserFactor() { if (/*如果是数字*/) { //注意考虑带符号数字 //parsNumber() } else if (/*如果是变量x*/) { //parserVar() } else { //是表达式因子 /*跳过左括号*/ Expr expr = parseExpr(); /*跳过右括号*/ return expr; } }
-
至此解析的工作基本完成,我们自上而下的已经得到了一个表达式树。
接下来就是构建部分,这里还是联系到了解析文法时的思路,我注意到一个表达式展开后有一个基本项(Monomial
),即a*x^b
的形式,任何一个输入进来的表达式展开后无非就是这些基本项的求和。我只需要把刚才构建的表达式树自下而上转化成一个多项式,再把多项式输出即可。由此我构建了一个多项式类Polynomial
具体实现如下:
-
Polynomial
中内部成员包含一个HashMap,用基本项中x的指数b
做key,系数a
做value,表示一个基本项序列,这些基本项之间的关系是相加关系。此处我用BigInteger
表示系数和指数的类型,这是因为展开后的数字大小可能会爆int甚至long。 -
我在
Polynomial
实现的方法包括添加一个基本项addMonoial()
,表达式相加addPolynomial()
,表达式相减subPolynomial()
,表达式相乘mulPolynomial()
。我在表达式加减法运算上考虑了同类项合并,以加法为例:Polynomial addPolynomial(Polynomial polynomial) { Polynomial ansPoly = new Polynomial(); //这里创建一个新的poly是为了防止深浅克隆出现问题 //将第一个加数全部放入ansPoly for (BigInteger exp : this.getPolynomial().keySet()) { BigInteger coef1 = this.getPolynomial().get(exp); if (/*如果ansPoly中已经有这个指数exp*/) { //可以合并同类项 BigInteger coef2 = polynomial.getPolynomial().get(exp); ansPoly.addMonomial(exp, coef2.add(coef1)); //实现相同指数的系数合并 } else { //否则直接把这个基本项加入ansPoly即可。 ansPoly.addMonomial(exp, coef1); } } /*再将第二个加数全部放入ansPoly*/ return ansPoly; }
-
接着在
Expr
、Term
、Factor
中实现buildPoly()
方法,通过从顶层Expr
类开始递归调用实现一个表达式树转化成一个多项式。buildPoly()
的实现过程中需要调用Polynomial
类中的加减乘方法。
最后是输出。输出的实现很简单,只需要重写Polynomial
类的toString()
方法,这里不再赘述。
可扩展性分析
-
采用递归下降的算法相比正则表达式匹配更具备可扩展性,可以轻松应对多层括号嵌套的问题(尽管第一次作业并没有允许多层括号,但后续迭代中支持了多层括号,这样的设计让我在第二次作业中没有重构解析这部分)。
-
因子类采用接口的策略也具备很好的可扩展性,如果后续添加新的因子种类那么可以直接接入
Factor
接口来实现。 -
构建阶段的基本项的思想也具备可扩展性,后续如果添加诸如三角函数、指数函数等,可以直接修改基本项实现。但由于本人第一次作业并没有构建一个基本项类,导致在第二次作业中遇到了麻烦。
优化策略
-
第一次作业的优化没有太多困难的,关键在于
toString()
之后的化简,需要仔细讨论每种情况。我所讨论的分类大致如下:if (/*系数 == 0*/) { /*输出0*/ } else if (/*系数 == -1*/) { /*只输出-*/ } else if (/*系数 == 1*/) { /*不输出系数*/ } else { /*输出系数*/ } if (/*指数 == 0*/) { /*如果系数为1或-1则补充数字1*/} else if (/*指数 == 1*/) { /*只输出x*/ } else { /*输出x^exp*/ }
-
此外需要注意,如果构建的多项式为空,那么其本质上就是数字0,因此需要进行特判。
-
另一个优化性能的细节是输出顺序的优化。如果一个表达式中有相加项也有相减项,那么优先输出相加关系的项会让输出长度变短。例如
x-2
会比-2+x
更短。我在第一次作业中执行的策略是如果第一项是负的,那就遍历寻找第一个为正的项,随即将前面的负数项挪到最后即可。这样优化已经可以确保第一次作业性能分拿满。if (sb.charAt(0) == '-') { for (int i = 0; i < sb.length(); i++) { if (sb.charAt(i) == '+') { return sb.substring(i + 1) + sb.substring(0, i); } } }
复杂度分析
方法复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Expr.Expr(ArrayList<Term>, ArrayList<Token>, String) | 0 | 1 | 1 | 1 |
Expr.buildPoly() | 10 | 2 | 6 | 6 |
Expr.toString() | 1 | 1 | 2 | 2 |
Lexer.Lexer(String) | 27 | 1 | 19 | 19 |
Lexer.back() | 0 | 1 | 1 | 1 |
Lexer.getNumber(String) | 5 | 3 | 3 | 4 |
Lexer.next() | 0 | 1 | 1 | 1 |
Lexer.notEnd() | 0 | 1 | 1 | 1 |
Lexer.now() | 0 | 1 | 1 | 1 |
Lexer.toString() | 1 | 1 | 2 | 2 |
Main.main(String[]) | 0 | 1 | 1 | 1 |
Number.Number(String) | 0 | 1 | 1 | 1 |
Number.buildPoly() | 0 | 1 | 1 | 1 |
Number.toString() | 0 | 1 | 1 | 1 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.parseExpr() | 5 | 2 | 6 | 6 |
Parser.parserTerm() | 4 | 1 | 5 | 5 |
Parser.paserFactor() | 7 | 5 | 6 | 6 |
Polynomial.Polynomial() | 0 | 1 | 1 | 1 |
Polynomial.addMonomial(BigInteger, BigInteger) | 0 | 1 | 1 | 1 |
Polynomial.addPolynomial(Polynomial) | 7 | 1 | 5 | 5 |
Polynomial.getPolynomial() | 0 | 1 | 1 | 1 |
Polynomial.mulPolynomial(Polynomial) | 7 | 1 | 4 | 4 |
Polynomial.subPolynomial(Polynomial) | 11 | 1 | 7 | 7 |
Polynomial.toString() | 35 | 10 | 16 | 17 |
Term.Term(ArrayList<Factor>) | 0 | 1 | 1 | 1 |
Term.buildPoly() | 1 | 1 | 2 | 2 |
Term.toString() | 1 | 1 | 2 | 2 |
Token.Token(Type, String) | 0 | 1 | 1 | 1 |
Token.getContent() | 0 | 1 | 1 | 1 |
Token.getType() | 0 | 1 | 1 | 1 |
Token.toString() | 0 | 1 | 1 | 1 |
Var.Var(String, String) | 0 | 1 | 1 | 1 |
Var.buildPoly() | 0 | 1 | 1 | 1 |
Var.toString() | 0 | 1 | 1 | 1 |
类复杂度分析
Class | OCavg | OCmax | WMC |
---|---|---|---|
Expr | 3.00 | 6 | 9 |
Lexer | 3.14 | 13 | 22 |
Main | 1.00 | 1 | 1 |
Number | 1.00 | 1 | 3 |
Parser | 3.25 | 5 | 13 |
Polynomial | 4.86 | 16 | 34 |
Term | 1.67 | 2 | 5 |
Token | 1.00 | 1 | 4 |
Token.Type | n/a | n/a | 0 |
Var | 1.00 | 1 | 3 |
- 可以看出来我的
Lexer
和Polynomial
类的复杂度很高,主要集中在构造函数Lexer.Lexer(String)
和Polynomial.toString()
方法。前者是因为要对输入的表达式进行预处理,后者是因为要化简输出形式,因此复杂度较高。不难看出无论是哪一步的化简都需要带来复杂度上的牺牲。
Bug分析
-
第一次作业我的强测没有发现bug,但是互测阶段被hack了一次,原因是我在
Lexer
类中处理预处理表达式时没有把左括号后的连续正负号处理干净。我当时的策略是记录上一个token
的内容,与当前token
一起分析。switch (curToken) { //以下==不代表真实代码中的==,且代码不全 case '+': if (preToken == '+' || preToken == '(') { /*不添加*/ } else if (preToken == '-') { remove(preToken); add(curToken); preToken == curToken; } /*其他情况省略*/ break; case '-': if (preToken == '+') { /*不添加*/ } else if (preToken == '-') { remove(preToken); add('+'); preToken == '+'; } /*其他情况省略*/ break; }
-
但是这样做显然会有bug,那就是针对
(-+-
时,我会多留一个+
变成(+
,而不能化简到最简(只有一个(
)。这样就会出现当parserTerm()
解析时检测到第一个token
是加号,并不能进行有效的解析。 -
解决办法是我直接在
parserTerm()
中加入了if
判断:如果找到加号就把数字1
添加进factors
,然后继续解析。这样做就不用修改Lexer
了。
小总结
第一次作业难度相比23年OO课程简单不少,只要注意细节处理过强测甚至拿满性能分也不是难事。不过进入互测就不能保证你能全身而退了。这里分享一下调试小技巧:可以重写一下各个类的toString()
方法,这样在IDEA上调试的时候控制台可以调用toString()
来显示各变量内容,同时也方便自己手动输出,可以更快定位是哪一步出现了问题。
02第二次作业
类图分析
还是先上类图
架构设计
第二次作业增加了指数函数和自定义函数。其中指数函数输入形式为exp(因子
)[^指数],自定义函数最多三个(fgh
),形参自变量最多三个(xyz
)。除此之外允许了多层括号嵌套(这部分hw1已经实现)。下面我将分别介绍我的迭代思路。
设计思路
首先是自定义函数。这部分的处理相对简单,主要是细节问题。
-
首先我构建了一个工具性质的类:
Definer
,用来读取自定义函数的定义,并存储每种自定义函数的形参列表paraList
和函数表达式funcList
。再构建一个CustomFunction
类用来表示自定义函数即可,内部成员存储了自定义函数名name
以及形参替换后的表达式funcExpr
(至于什么时候完成函数形参替换后面再说)。根据文法,CustomFunction
类是属于因子的,因此接入Factor
接口。 -
Definer
类中我主要实现了两个方法:addFunc()
和callFunc()
。addFunc()
用在读取自定义函数定义时使用,向Definer
中存储其形参列表和函数表达式。callFunc()
用在替换形参时使用,传入函数名和对应的实参列表,返回一个形参替换后的表达式。替换过程直接通过字符串的
replaceAll()
实现,但要格外注意两点:一是指数函数exp
中的x
不要被替换掉,二是警惕先替换非x
形参再替换x
所引发的问题。前者很好理解,后者我举一个例子就好:如果f
定义为f(y,x)=x^2+exp(y)
,表达式中需要处理f(x,x^2)
,那么如果不做特别处理且替换过程是按形参读入顺序替换的话就会出现以下情况:第一步替换:将
y
替换:x^2+exp(x)
第二部替换:将
x
替换:(x^2)^2+exp(x^2)
而真正的答案应该是
(x^2)^2+exp(x)
,这就是由于函数形参和实参中都有x
所引发的问题。处理办法也很简单,那就是在替换前将形参替换成另一套形参。String callFunc(String name, ArrayList<String> para) { String funcExpr = funcList.get(name); funcExpr = funcExpr.replaceAll("exp","A"); //保护exp funcExpr = funcExpr.replaceAll("x","u"); funcExpr = funcExpr.replaceAll("y","v"); funcExpr = funcExpr.replaceAll("z","w"); /* 再实现形参替换*/ return funcExpr.replaceAll("A", "exp"); }
-
下面重点谈一下我在什么时候完成形参替换的。考虑到一是
Lexer
类的复杂度已经很高,所以不宜在处理Token
流时实现替换,二是从设计思路的角度出发,自定义函数本身就是一类因子,完全可以在解析时实现替换,也更符合单一职责设计原则(SRP)。所以我在
Parser
类中实现了parserCustom()
方法,返回一个CustomFunction
类的因子。parserCustom()
中我调用了parserFactor()
和toString()
方法来返回实参。这样做的好处是,如果函数调用的实参因子中有函数,那么可以通过parserFactor()
来自动解析这个作为实参因子的函数并返回一个解析之后的可替换的String
类型的实参。CustomFunction parserCustom(Token token) { String funcName = token.getContent(); // token的内容为函数名 /*跳过(*/ ArrayList<String> actPara = new ArrayList<>(); //实参 //根据函数名找到目标函数 for (/*根据目标函数的形参数量记录对应实参*/) { actPara.add(paserFactor().toString()); /*跳过,*/ } return new CustomFunction(funcName, actPara); }
可以看到我的
CustomFunction
类构造函数传入了两个参数,一个是函数名funcName
,一个是形参列表actPara
。紧接着我在CustomFunction
类构造函数中调用Definer
类的callFunc()
方法,得到一个形参替换完成后的表达式expr
(String
类型),再调用Parser
类解析这个expr
即可得到替换后的表达式funcExpr
(Expr
类型)。 -
完成自定义函数的解析之后,
CustomFunction
类的buildPoly()
方法直接调用funcExpr.buildPoly()
就好。
其次是指数函数,这部分解析也很简单,直接构建一个新类ExpFunction
存储exp(因子
)里的因子就好。特别提一句如果exp
外面有指数的话,我直接在解析中把指数和里面的因子乘在了一起。同样把ExpFunction
接入Factor
接口。
不过加入了指数函数之后,基本项就要发生变化,由原来a*x^b
变成a*x^b*exp(poly)
。我干脆直接加入了一个新类Monomial
(单项式)用来描述基本项,原来的Polynomial
类成员变量改为一个ArrayList<Monomial>
。Monomial
类成员变量有系数coefficient
,x
的指数exp
和指数函数括号内的Poly
。而Number
、Var
和ExpFunction
类buildPoly()
方法也变成了先构建Monomial再返回Polynomial,其他类的buildPoly()
实现基本不变。
可扩展性分析
虽然本次作业保证函数表达式在定义时不会出现自己或其他自定义函数,但用Definer
实现函数替换可以轻松实现用函数定义函数的情况的形参替换,并且可以通过CustomFunction
的构造函数调用Parser
类解析表达式时解决函数表达式定义中有函数的问题。
优化策略
-
第二次作业由于基本项增加了指数函数,
exp(0)=1
,所以可以判断指数函数内表达式是否为0
进而化简输出。 -
之前调整输出顺序的策略是如果第一项为负就寻找第一个
+
,然后把+
号以前的挪到最后。但这次找到的+
号可能是属于指数函数内表达式的,不能盲目挪位置。所以这次我更改了输出策略:优先输出正项,输出完正项再输出负项。 -
理论上指数函数括号内可以再嵌套一层括号,例如
exp((x))
。但指数函数括号内的因子如果是非表达式因子时可以只带一层括号,例如exp(x^2)
、exp(1)
。(如果为表达式因子则必须输出必要的表达式因子括号)。因此我写了一个isFactor()
方法来判断是否可以化简掉一层括号。boolean isFactor() { int flag = 0; //标记是否出现过非表达式因子 boolean isFactor = true; for (int i = 0; i < polynomial.size(); i++) { Monomial monomial = polynomial.get(i); if (monomial.getCoefficient() != 0) { //只判断非0项即可 if (flag > 0) { return false; //如果非表达式个数大于1,那么一定是表达式因子 } else { flag = 1; isFactor = /*判断是不是非表达式因子*/ } } } return isFactor; //如果多项式==0,那么也自动返回True }
-
多项式内合并同类项也发生在多项式加减法的部分,我写了一个判断
x
指数和指数函数内多项式均相等的方法hasSameExp()
。这个方法内部我调用了equals()
方法,因此需要重写Polynomial
类的equals()
方法,使其判断多项式内的每一个单项式都相等。boolean hasSameExp(Monomial monomial) { return exp.equals(monomial.getExp()) && expPoly.equals(monomial.getExpPoly()); }
-
本次作业由于时间关系我并没有实现提公因数化简指数函数的方法,只做了上述化简的基础上也能在强测拿到97分。不过在第三次作业中我实现了这一策略,感兴趣的同学可以直接移步至第三次作业优化策略部分。
-
由于时间关系,本次作业我没有做更多化简,最终在强测性能评估中也拿下了97.5分。在第三次作业中我补充实现了提公因数的化简策略(可移步至第三次作业优化策略部分)。
复杂度分析
方法复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
“CustomFunction.CustomFunction(String, ArrayList<String>)” | 0 | 1 | 1 | 1 |
CustomFunction.buildPoly() | 0 | 1 | 1 | 1 |
CustomFunction.getFuncExpr() | 0 | 1 | 1 | 1 |
CustomFunction.toString() | 0 | 1 | 1 | 1 |
Definer.addFunc(String) | 1 | 1 | 2 | 2 |
“Definer.callFunc(String, ArrayList<String>)” | 1 | 1 | 2 | 2 |
Definer.funcParaNum(String) | 0 | 1 | 1 | 1 |
Definer.toMyString() | 3 | 1 | 3 | 3 |
Definer.varEqual(String) | 4 | 4 | 3 | 4 |
ExpFunction.ExpFunction(Expr) | 0 | 1 | 1 | 1 |
ExpFunction.buildPoly() | 0 | 1 | 1 | 1 |
ExpFunction.toString() | 0 | 1 | 1 | 1 |
“Expr.Expr(ArrayList<Term>, ArrayList<Token>, String)” | 0 | 1 | 1 | 1 |
Expr.buildPoly() | 11 | 2 | 7 | 7 |
Expr.toString() | 2 | 1 | 3 | 3 |
Lexer.Lexer(String) | 27 | 1 | 18 | 18 |
Lexer.back() | 0 | 1 | 1 | 1 |
Lexer.enableToAdd(Token) | 1 | 1 | 3 | 3 |
Lexer.getExpFunc() | 0 | 1 | 1 | 1 |
Lexer.getNumber(String) | 5 | 3 | 3 | 4 |
Lexer.isFunc(String) | 1 | 1 | 3 | 3 |
Lexer.isVar(String) | 1 | 1 | 3 | 3 |
Lexer.next() | 0 | 1 | 1 | 1 |
Lexer.notEnd() | 0 | 1 | 1 | 1 |
Lexer.now() | 0 | 1 | 1 | 1 |
“Lexer.replaceAll(HashMap<String, ArrayList<Token>>)” | 3 | 1 | 3 | 3 |
Lexer.toString() | 1 | 1 | 2 | 2 |
Main.main(String[]) | 1 | 1 | 2 | 2 |
“Monomial.Monomial(String, String, Polynomial)” | 0 | 1 | 1 | 1 |
Monomial.addCoefficient(Monomial) | 0 | 1 | 1 | 1 |
Monomial.equals(Object) | 4 | 3 | 4 | 6 |
Monomial.getCoefficient() | 0 | 1 | 1 | 1 |
Monomial.getExp() | 0 | 1 | 1 | 1 |
Monomial.getExpFactor() | 0 | 1 | 1 | 1 |
Monomial.hasSameExp(Monomial) | 1 | 1 | 2 | 2 |
Monomial.isExp() | 0 | 1 | 1 | 1 |
Monomial.isNumber() | 6 | 4 | 4 | 6 |
Monomial.isPositive() | 0 | 1 | 1 | 1 |
Monomial.isVar() | 4 | 4 | 2 | 4 |
Monomial.mulMono(Monomial) | 0 | 1 | 1 | 1 |
Monomial.negate() | 0 | 1 | 1 | 1 |
Monomial.toString() | 25 | 2 | 19 | 20 |
Number.Number(String) | 0 | 1 | 1 | 1 |
Number.buildPoly() | 0 | 1 | 1 | 1 |
Number.toString() | 0 | 1 | 1 | 1 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.parseExpr() | 5 | 2 | 6 | 6 |
Parser.parserCustom(Token) | 1 | 1 | 2 | 2 |
Parser.parserExp() | 2 | 1 | 3 | 3 |
Parser.parserTerm() | 4 | 1 | 5 | 5 |
Parser.paserFactor() | 9 | 7 | 8 | 8 |
Polynomial.Polynomial() | 0 | 1 | 1 | 1 |
Polynomial.addMonomial(Monomial) | 3 | 3 | 3 | 3 |
Polynomial.addPolynomial(Polynomial) | 4 | 1 | 4 | 4 |
Polynomial.equals(Object) | 9 | 7 | 4 | 8 |
Polynomial.equalsToZero() | 4 | 4 | 2 | 4 |
Polynomial.getPolynomial() | 0 | 1 | 1 | 1 |
Polynomial.hasMonomial(Monomial) | 3 | 3 | 2 | 3 |
Polynomial.isFactor() | 7 | 4 | 4 | 4 |
Polynomial.mulPolynomial(Polynomial) | 6 | 1 | 4 | 4 |
Polynomial.subPolynomial(Polynomial) | 4 | 1 | 4 | 4 |
Polynomial.toString() | 6 | 2 | 4 | 5 |
Term.Term(ArrayList<Factor>) | 0 | 1 | 1 | 1 |
Term.buildPoly() | 2 | 1 | 3 | 3 |
Term.toString() | 1 | 1 | 2 | 2 |
“Token.Token(Type, String)” | 0 | 1 | 1 | 1 |
Token.getContent() | 0 | 1 | 1 | 1 |
Token.getType() | 0 | 1 | 1 | 1 |
Token.toString() | 0 | 1 | 1 | 1 |
“Var.Var(String, String)” | 0 | 1 | 1 | 1 |
Var.buildPoly() | 0 | 1 | 1 | 1 |
Var.toString() | 0 | 1 | 1 | 1 |
不难看出,这次Polynomial.toString()
的复杂度转移到了Monomial.toString()
上。其余复杂度并无太多变化。说明本次迭代开发的复杂度得到了有效的控制。
类复杂度
Class | OCavg | OCmax | WMC |
---|---|---|---|
CustomFunction | 1.00 | 1 | 4 |
Definer | 2.40 | 4 | 12 |
ExpFunction | 1.00 | 1 | 3 |
Expr | 3.67 | 7 | 11 |
Lexer | 2.58 | 15 | 31 |
Main | 2.00 | 2 | 2 |
Monomial | 2.50 | 14 | 35 |
Number | 1.00 | 1 | 3 |
Parser | 3.17 | 7 | 19 |
Polynomial | 3.64 | 7 | 40 |
Term | 2.00 | 3 | 6 |
Token | 1.00 | 1 | 4 |
Token.Type | n/a | n/a | 0 |
Var | 1.00 | 1 | 3 |
bug分析
- 本次作业我的强测和互测均没有测出bug,也没成功hack别人。
- 不过代码编写过程中出现了一些问题。
- 首先是函数形参替换时,个别实参忘记添加必要括号导致的bug。例如定义
f(x)=x^2
,我在一开始解析f(x^2)
时由于缺少必要的括号导致形参替换后的表达式变为了x^2^2
,这是不符合文法的。正确做法是添加必要的括号(x^2)^2
。 - 其次最严重的深浅克隆问题。这里总结一条:只要是涉及多项式或者单项式加减乘、取反等方法一律使用深克隆,不要改变任何已有单项式或者多项式的成员变量。最便捷的办法是重写
clone()
方法,使其能够克隆一个内部成员变量也相同的新对象。
- 首先是函数形参替换时,个别实参忘记添加必要括号导致的bug。例如定义
小总结
第二次作业难度相比第一次有了新一个台阶式的上升,如果第一次作业架构的可扩展性差的话那很可能第二次作业就会面临重构。
03第三次作业
类图分析
最后一张
架构设计
第三次作业增加了求导因子。求导因子的形式化表示为dx(表达式)|dx(求导因子)
。本次作业中允许函数定义式出现自定义函数(由于之前的架构已经可以处理所以本次作业并不需要重构)。总体任务难度较小。
设计思路
针对求导因子我构建了Derivative
类用来存解析得到的求导因子,内部成员只有一个Expr
类型的表达式expr
。Derivative
类接入Factor
接口。至于求导因子的解析比较简单,这里不再赘述。
重点谈一下求导方法的编写。首先我在Expr
、Term
、Factor
中均实现了求导derive()
方法,通过顶层Expr.derive()
方法递归调用其他类的求导方法。具体的求导逻辑参考基本函数的求导公式实现,并不复杂。唯一需要说明的是本次我的derive()
方法返回值均为Factor
类型,并且可以看到我把Term
类也接上了Factor
接口。这是受第二次课上实验代码的启发,这样可以很好的统一求导方法的返回值类型为Factor
,不然试想一下:如果各类求导方法的返回值类型各不相同,那么面对求导因子嵌套的情景时可能就会出现意想不到的解析错误。(比如我一开始Term
求导返回的是Expr
类型,而Expr
求导返回Factor
类型。那么如果对一个term
求导返回expr
,此时如果再求一个导就变成了Factor
类型,在自己的代码中可能会出现意想不到的bug)。
//求导方法
//d(expr) = d(term0)+d(term1)+…………
expr.derive() = expr.getTerm(0).derive() + expr.getTerm(1).derive + …………;
//d(Term) = d(factor0)*factor1*factor2 + factor0*d(factor1)*factor2 + factor0*factor1*d(factor2)
term.derive() = term.getFactor(0).derive() * term.getFactor(1) * term.getFactor(2) * …………
+ term.getFactor(0) * term.getFactor(1).derive() * term.getFactor(2) * …………
+ term.getFactor(0) * term.getFactor(1) * term.getFactor(2).derive() * …………
+ …………;
factor.derive() = expr.derive() | term.derive() | number.derive() | var.derive()
| expFunction.derive() | customFunction.derive() | derivative.derive();
注意!derive()
方法需要深克隆。
可扩展性分析
最后一次作业了就不分析啥了。
优化策略
-
我主要实现了针对指数函数括号内表达式的提公因数优化。举个例子,将原本
exp((100*x+100))
优化为exp((x+1))^100
。但盲目提取最大公因数有时候并不会优化长度,甚至会让长度更长(例如exp((20*x+30)))
提取后变成exp((2*x+3))^10
反而变长了)。但遍历每个公因数寻找最小情况开销太大并且性价比太低。因此我还是采取提取最大公因数然后比较长度变化来判断是否化简。StringBuilder simplifyForExp(StringBuilder stringBuilder) { StringBuilder sb = new StringBuilder(stringBuilder); BigInteger bigGcd = this.getGcd(); //获取最大公因数 if (bigGcd.compareTo(BigInteger.ONE) == 0 || bigGcd.compareTo(BigInteger.ZERO) == 0) { /*不用提取直接输出*/ } for (Monomial monomial : polynomial) { //每个项的系数除以公因数 newPoly.addMonomial(monomial.divideCoefficient(bigGcd)); } sb = /*输出提取公因数之后的形式*/ return sb; }
-
其中,
getGcd()
方法的实现调用了BigInteger
自带的gcd()
方法来获取所有单项式系数的最大公因数。接着比较提取公因式之后和提取之前的长度,判断要不要提取公因数。BigInteger getGcd() { BigInteger bigGcd = /*获取所有单项式系数的最大公因数*/ /*这里如果得到bigGcd == 0 或 1 可以直接返回*/ int bestLength = bigGcd.toString().length() + 1; // 这部分长度表示 "^bigGcd".length() int worstLength = 0; for (Monomial monomial : polynomial) { BigInteger initCoef = monomial.getCoefficient(); worstLength += initCoef.toString().length(); BigInteger newCoef = initCoef.divide(bigGcd); //此处理论上需要分情况讨论(bigGcd = 1 or -1 or else)bestLength bestLength += newCoef.toString().length(); } if (bestLength < worstLength) { return bigGcd; } //以上为示意过程,实际上还需要注意很多情况 /*例如如果polynomial.size() == 1,那么可能提取公因数之后会少一层括号 如exp((2*x))提取公因数2后变成exp(x)^2,在计算worstLength时要额外+2*/ /*还有避免提取出来的公因数为负数的情况 如exp((-2*x))不能变成exp(x)^-2(不符合形式化表达)*/ return BigInteger.ZERO; }
-
我还对一些类型的
buildPoly()
方法进行了性能优化。比如我特意定义了关键字Polynomial.Zero
表示0
多项式。计算多项式相乘时,如果其中一个因子是0
因子,就直接返回Polynomial.Zero
;计算多项式相加减时,如果一个项是0
项,就直接不进行加减运算。同理定义了关键字Monomial.Zero
表示0
单项式,也化简了单项式相乘和合并同类项方法的复杂度。 -
其实针对指数函数化简还有更复杂的化简策略,例如
exp((12+3333333*x+9999999*x^2))
(长度为31)如果采用提最大公因数策略会得到exp((4+1111111*x+3333333*x^2))^3
此时由于长度没有变小(长度为32)因此不会优化;但实际上会有更短的输出exp(12)*exp((x+3*x^2))^3333333
(长度为30)。但考虑时间成本,性价比最高的就是上述优化策略。能做好这部分优化基本上也能拿到一个客观的分数了(本人除了一个点被某位大佬拉爆以外全拿满100)。
复杂度分析
方法复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
CustomFunction.CustomFunction(String, ArrayList<String>) | 0 | 1 | 1 | 1 |
CustomFunction.CustomFunction(String, Expr) | 0 | 1 | 1 | 1 |
CustomFunction.buildPoly() | 0 | 1 | 1 | 1 |
CustomFunction.clone() | 0 | 1 | 1 | 1 |
CustomFunction.derive() | 0 | 1 | 1 | 1 |
CustomFunction.getFuncExpr() | 0 | 1 | 1 | 1 |
CustomFunction.toString() | 0 | 1 | 1 | 1 |
Definer.addFunc(String) | 1 | 1 | 2 | 2 |
Definer.callFunc(String, ArrayList<String>) | 1 | 1 | 2 | 2 |
Definer.funcParaNum(String) | 0 | 1 | 1 | 1 |
Definer.toMyString() | 3 | 1 | 3 | 3 |
Definer.varEqual(String) | 4 | 4 | 3 | 4 |
Derivative.Derivative(Expr) | 0 | 1 | 1 | 1 |
Derivative.buildPoly() | 0 | 1 | 1 | 1 |
Derivative.clone() | 0 | 1 | 1 | 1 |
Derivative.derive() | 0 | 1 | 1 | 1 |
Derivative.toString() | 0 | 1 | 1 | 1 |
ExpFunction.ExpFunction(Expr) | 0 | 1 | 1 | 1 |
ExpFunction.buildPoly() | 0 | 1 | 1 | 1 |
ExpFunction.clone() | 0 | 1 | 1 | 1 |
ExpFunction.derive() | 0 | 1 | 1 | 1 |
ExpFunction.toString() | 0 | 1 | 1 | 1 |
Expr.Expr(ArrayList<Term>, ArrayList<Token>, String) | 0 | 1 | 1 | 1 |
Expr.Expr(String) | 0 | 1 | 1 | 1 |
Expr.addTerm(Term) | 1 | 1 | 2 | 2 |
Expr.addTerm(Term, Token) | 0 | 1 | 1 | 1 |
Expr.buildPoly() | 10 | 2 | 6 | 6 |
Expr.clone() | 2 | 1 | 3 | 3 |
Expr.derive() | 3 | 2 | 4 | 4 |
Expr.opsClone() | 0 | 1 | 1 | 1 |
Expr.termsClone() | 0 | 1 | 1 | 1 |
Expr.toString() | 2 | 1 | 3 | 3 |
Lexer.Lexer(String) | 31 | 1 | 19 | 19 |
Lexer.back() | 0 | 1 | 1 | 1 |
Lexer.enableToAdd(Token) | 1 | 1 | 3 | 3 |
Lexer.getDerive() | 0 | 1 | 1 | 1 |
Lexer.getExpFunc() | 0 | 1 | 1 | 1 |
Lexer.getNumber(String) | 5 | 3 | 3 | 4 |
Lexer.isFunc(String) | 1 | 1 | 3 | 3 |
Lexer.isVar(String) | 1 | 1 | 3 | 3 |
Lexer.next() | 0 | 1 | 1 | 1 |
Lexer.notEnd() | 0 | 1 | 1 | 1 |
Lexer.now() | 0 | 1 | 1 | 1 |
Lexer.toString() | 1 | 1 | 2 | 2 |
Main.main(String[]) | 1 | 1 | 2 | 2 |
Monomial.Monomial(String, String, Polynomial) | 0 | 1 | 1 | 1 |
Monomial.addCoefficient(Monomial) | 0 | 1 | 1 | 1 |
Monomial.divideCoefficient(BigInteger) | 0 | 1 | 1 | 1 |
Monomial.equals(Object) | 4 | 3 | 4 | 6 |
Monomial.equalsToZero() | 0 | 1 | 1 | 1 |
Monomial.expToString(Polynomial, StringBuilder) | 0 | 1 | 1 | 1 |
Monomial.getCoefficient() | 0 | 1 | 1 | 1 |
Monomial.getExp() | 0 | 1 | 1 | 1 |
Monomial.getExpFactor() | 0 | 1 | 1 | 1 |
Monomial.hasSameExp(Monomial) | 1 | 1 | 2 | 2 |
Monomial.isExp() | 0 | 1 | 1 | 1 |
Monomial.isNumber() | 6 | 4 | 4 | 6 |
Monomial.isPositive() | 0 | 1 | 1 | 1 |
Monomial.isVar() | 4 | 4 | 2 | 4 |
Monomial.mulMono(Monomial) | 1 | 2 | 1 | 2 |
Monomial.negate() | 0 | 1 | 1 | 1 |
Monomial.toString() | 18 | 2 | 17 | 18 |
Number.Number(String) | 0 | 1 | 1 | 1 |
Number.buildPoly() | 0 | 1 | 1 | 1 |
Number.clone() | 0 | 1 | 1 | 1 |
Number.derive() | 0 | 1 | 1 | 1 |
Number.toString() | 0 | 1 | 1 | 1 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.parseExpr() | 5 | 2 | 6 | 6 |
Parser.parserCustom(Token) | 1 | 1 | 2 | 2 |
Parser.parserExp() | 2 | 1 | 3 | 3 |
Parser.parserTerm() | 4 | 1 | 5 | 5 |
Parser.paserFactor() | 10 | 8 | 9 | 9 |
Polynomial.Polynomial() | 0 | 1 | 1 | 1 |
Polynomial.Zero() | 0 | 1 | 1 | 1 |
Polynomial.addMonomial(Monomial) | 4 | 4 | 3 | 4 |
Polynomial.addPolynomial(Polynomial) | 4 | 1 | 4 | 4 |
Polynomial.clone() | 0 | 1 | 1 | 1 |
Polynomial.equals(Object) | 9 | 7 | 4 | 8 |
Polynomial.equalsToZero() | 4 | 4 | 2 | 4 |
Polynomial.getGcd() | 21 | 6 | 7 | 13 |
Polynomial.getPolynomial() | 0 | 1 | 1 | 1 |
Polynomial.hasMonomial(Monomial) | 3 | 3 | 2 | 3 |
Polynomial.isFactor() | 7 | 4 | 4 | 4 |
Polynomial.mulPolynomial(Polynomial) | 5 | 2 | 5 | 5 |
Polynomial.simplifyForExp(StringBuilder) | 8 | 2 | 6 | 6 |
Polynomial.subPolynomial(Polynomial) | 4 | 1 | 4 | 4 |
Polynomial.toString() | 6 | 2 | 4 | 5 |
Term.Term(ArrayList<Factor>) | 0 | 1 | 1 | 1 |
Term.Term(Factor) | 0 | 1 | 1 | 1 |
Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
Term.buildPoly() | 4 | 4 | 4 | 4 |
Term.clone() | 1 | 1 | 2 | 2 |
Term.derive() | 6 | 1 | 4 | 4 |
Term.toString() | 1 | 1 | 2 | 2 |
Token.Token(Type, String) | 0 | 1 | 1 | 1 |
Token.clone() | 0 | 1 | 1 | 1 |
Token.getContent() | 0 | 1 | 1 | 1 |
Token.getType() | 0 | 1 | 1 | 1 |
Token.toString() | 0 | 1 | 1 | 1 |
Var.Var(String, String) | 0 | 1 | 1 | 1 |
Var.buildPoly() | 0 | 1 | 1 | 1 |
Var.clone() | 0 | 1 | 1 | 1 |
Var.derive() | 1 | 1 | 2 | 2 |
Var.toString() | 0 | 1 | 1 | 1 |
Polynomial.getGcd()
方法复杂度非常高,主要是由于内部调用BigInteger.gcd()
频繁,并且调用大量toString()
方法。这告诉我们或许输出性能和代码处理性能的调和是很重要的,不能因为过度追求输出性能而导致代码复杂度爆炸。Lexer
构造函数复杂度由于求导因子的加入再创新高,所以其实把预处理的功能拆分出来是降低复杂度的好方法。Monomial.toString()
由于调用许多其他方法并且内部进行了分类讨论所有复杂度也很高。
类复杂度分析
Class | OCavg | OCmax | WMC |
---|---|---|---|
CustomFunction | 1.00 | 1 | 7 |
Definer | 2.40 | 4 | 12 |
Derivative | 1.00 | 1 | 5 |
ExpFunction | 1.00 | 1 | 5 |
Expr | 2.30 | 6 | 23 |
Lexer | 2.50 | 16 | 30 |
Main | 2.00 | 2 | 2 |
Monomial | 2.18 | 12 | 37 |
Number | 1.00 | 1 | 5 |
Parser | 3.33 | 8 | 20 |
Polynomial | 4.07 | 13 | 61 |
Term | 2.14 | 4 | 15 |
Token | 1.00 | 1 | 5 |
Token.Type | n/a | n/a | 0 |
Var | 1.20 | 2 | 6 |
- 实现提公因数化简之后的
Polynomial
类的WMC变得更大了,平均循环复杂度更上一层楼。或许可以新建一个Optimizer
类负责优化工作,减轻Polynomial
的复杂度。
bug分析
- 本次作业我的强测和互测均未测出bug。但第三次作业明显大家的"杀心"变强了。我也成功在这次作业中hack中了同学。(事后发现被hack的是好哥们)
- 作业过程中也只出现了处理求导因子嵌套时无法得到想要的返回值类型的问题(见设计思路部分)。解决方案就是让
Term
类求导返回值类型也变成Factor
,实现了所有求导方法返回值统一为Factor。进而解决了这个问题。 - hack策略详见04hack策略。
小总结
第三次作业圆满收官,总结下来第一单元的课程难度相比去年下降不少,但还保持着一定的挑战性,能一次不被hack也是很难的事情。随着迭代开发的推进,要有意控制代码编写的复杂度,注意适当降低方法的耦合度,逐渐形成打包的思想,还要逐步积累调试经验,注意迭代开发的可扩展性。
04hack策略
- 虽然本人只在第三次作业中成功hack其他人,但还是想分享一些hack思路以及心得体会。思路针对自己构造样例,如果你使用测评机,那你也可以在测评机中随机构造一些这样的特殊样例来强化数据。
- 基本上分为三个阶段。第一是简单阶段。构造一些极其简单看似不会出错的样例。例如
0
、x-x
、exp(0)
、-1
、-exp(x)
、f(-1)
等。尽量结合自己错过的案例。第二是基础阶段。构造一些复合样例。例如f(dx(x))
、g(f(x),exp(x)^2)
、dx(exp(exp(exp(exp(x)))))
。这些样例有些可以测出bug,有些可以让对面TLE
。第三个阶段就上测评机,靠大量测试样例轰击代码,总有一款“适合”他。 - 注意不要刀上头了!同质bug出刀过多是会被处罚的。
05心得体会
Unit1感想
首先不得不说OO这门课的作业形式确实是别具一格的,真的是机会与挑战并存。机会是提升自己能力的机会,挑战是对任务完成度和完善度的挑战。记得第一次作业抓耳挠腮想了整整一天也没想出解析之后该如何化简表达式,那个时候感觉自己的OO快要完了。后来第二天放空大脑休息了一个下午之后就茅塞顿开了。这里也得特别感谢一下往期学长的博客,确实是对我帮助很大。第二次作业又一下子上了一波强度,那一周也是手忙脚乱,最后赶在周五写出了第一版代码,然后开始疯狂debug,不过好在当天下午就过中测了。第三次作业则较为简单,算是给紧张刺激的前两周打了一针镇定剂。然而最紧张刺激的还得是hack环节。每次刷新都伴随着莫名的紧张,看着自己的受到hack从0/0变到0/40,突然变成1/41——顿时想起那句梗:“我终于释怀的()了”。总体上对自己这一单元的表现还是很满意的,满分100我愿意给自己打95,一分扣给被hack,一分扣给性能优化还可以更好,三分扣给没写出测评机。希望下一单元的自己能再接再厉吧!
未来展望
- 希望后续的作业不再拖延,这样能给自己留出足够的debug和优化性能的时间。
- 希望未来在设计代码上能更靠近
SOLID
设计原则,写出质量更高的代码 - 写出测评机!!!!!