BUAA-OO-第一单元总结
前言
第一单元的主要目的是实现表达式的展开,体会层次化设计的思想的应用和工程实现。
- 第一次作业:读入一个包含加、减、乘、乘方以及括号(只可能出现单层括号)的表达式,输出去括号展开后的结果。
- 第二次作业:在第一次作业基础上增加了多层括号嵌套以及自定义函数、指数函数。
- 第三次作业:在第二次作业基础上允许自定义函数定义时调用已定义的其他自定义函数,并增加求导算子。
第一次作业分析
UML类图
思路分析
通过对第一次作业的分析,我们梳理思路,可以将本次作业的任务分为两部分——表达式解析与表达式展开(计算)。表达式由三部分构成,分别是Expr
,Term
,Factor
,我在Expr
中用ArrayList
来存储属于它的Term
,在Term
中同样使用ArrayList
来存储属于它的Factor
,而Factor
又分为三种,分别是NumFactor
(常数因子),PowerFactor
(幂函数因子),Expr
(表达式因子)。由于三种因子具有行为共性,我创建了Factor
接口,并让三个因子类实现该接口。根据刚刚所说的结构进行基础类的创建,建类之后,让我们再来思考如何解决表达式解析与表达式展开这两个问题。
表达式解析
两种思路 正则表达式 or 递归下降
对于解析表达式,有两种方法,其一是正则表达式,其二是递归下降。在最开始思考时,我觉得正则表达式思考难度低,且pre课程有过相关知识的学习,决定采用正则表达式,但考虑到第二次、第三次作业的迭代,正则表达式可能不具备很好的可扩展性,且与大部分同学的方法不同,我最终还是决定采用课程组推荐的递归下降算法。
具体实现
首先我建立了Processor
类,该类的作用是对输入字符串进行预处理。我们最开始得到的表达式是一个含有空白符和连续±号的字符串,预处理类Processor
可以删去表达式中的空白符,并将连续的±号合并成一个,为后续处理提供便利。
其次就是递归下降的核心,Lexer
类和Parser
类。Lexer
类的作用是词法分析,可以将传入的表达式分解成独立的语法单元;Parser
类的作用则是依据形式化表述,对Lexer
类分析出的语法单元进行解析,递归生成表达式、项和因子。
Lexer的实现
Lexer
类接收一个字符串,通过其内部的方法对该字符串进行分解,并返回独立的语法单元。
public void next() {
if (pos == input.length()) {
return;
}
char c = input.charAt(pos);
if (Character.isDigit(c)) {
curToken = getNumber();
} else if (!Character.isDigit(c)) {
pos += 1;
curToken = String.valueOf(c);
}
}
next
方法控制词法分析的位置,每当一个语法单元分解完成,next
方法使词法分析器移动至下一语法单元。
public String peek() {
return this.curToken;
}
peek
方法可以返回当前词法分析器分解的语法单元,交与Parser
类进行解析处理。
Parser的实现
Parser
类中的parseExpr
方法,parseTerm
方法,parseFactor
方法对应解析的三部分——表达式、项、因子,每一部分的解析都严格遵循形式化表述。
以parseFactor
方法为例简要介绍。
public Factor parseFactor() {
if (lexer.peek().equals("(")) {
lexer.next();
Factor expr = parseExpr();
lexer.next();
if (lexer.peek().equals("^")) {
lexer.next();
expr.setExp(Integer.parseInt(lexer.peek()));
lexer.next();
}
return expr;
} else if (lexer.peek().equals("x")) {
String x = lexer.peek();
lexer.next();
int exp = 1;
if (lexer.peek().equals("^")) {
lexer.next();
exp = Integer.parseInt(lexer.peek());
lexer.next();
}
return new PowerFactor(x, exp);
} else {
BigInteger num = new BigInteger(lexer.peek());
lexer.next();
return new NumFactor(num);
}
}
public Expr parseExpr() {...}
public Term parseTerm(int sign) {...}
三个分支对应三种因子——表达式因子、幂函数因子、常数因子,解析完成后,返回各个因子对应的对象。parseExpr
方法和parseTerm
方法类似,将Expr
对应的Term
与Term
对应的Factor
加入各自ArrayList
之后,还需要注意符号的处理,我对每个Term
添加了sign
属性,确保正负处理的正确性。
表达式展开
基于数学知识,我们可以知道展开的最终结果是一个多项式的形式,即:
而多项式由单项式构成,单项式含有系数与指数两项属性。因此我们可以再建立两个类,分别是Poly
类(多项式)和Mono
类(单项式)。
Mono
类含有两个属性,coe
与exp
,分别代表单项式的系数和指数。
private BigInteger coe;
private int exp;
Mono
类还应含有toString()
方法,可以将Mono
转换成**“coe*x^exp”**的形式。
Ploy
类由Mono
类构成,因此在Ploy
类中构造一个ArrayList
,用以存储属于它的一系列Mono
,Poly
类中还含有addPoly()
,mulPoly()
等方法,用以支持多项式的加法与乘法运算(幂运算可以转化为乘法运算),最后用toString()
方法将Mono
的字符串形式连接起来,形成最终的表达式字符串。
private ArrayList<Mono> monoList;
//.....
public Poly addPoly(Poly poly) {...}
public Poly mulPoly(Poly poly) {...}
public String toString() {...}
到这里还不够,我们还需要想办法将Expr
类,Term
类,Factor
类中的内容转化成多项式。通过思考,我在Expr
类,Term
类,Factor
类中分别实现了toPloy()
方法,最后可以从底层的factor.toPoly()
向上转化,通过顶层的expr.toPoly()
获取最终结果。
具体过程:
NumFactor
和PowerFactor
的toPoly()
方法最简单,只需转化为仅含有一个Mono
的Poly
即可。例如,数字因子2
可以转化为仅含有单项式2*x^0
的多项式,幂函数因子x^2
可以转化为仅含有单项式1*x^2
的多项式。Expr
既可作为表达式,又可作为因子,因此我在Expr
类中增加exp
属性(默认值为-1),在将其所含的所有Term
的Poly
形式用addPoly()
方法加起来后,利用mulPoly()
方法即可得到幂次计算后的Poly
,下为Expr
的toPoly()
方法:
//...
private int exp;
//...
public Poly toPoly() {
// ...
for (Term it : terms) {
poly = poly.addPoly(it.toPoly());
}
if (exp != -1) {
if (exp == 0) {
// ...
} else {
// ...
}
}
return poly;
}
- 类比
Expr
的toPoly()
方法,Term
类的toPoly()
方法为:将其所含的所有Factor
的Poly
形式用mulPoly()
方法乘起来。下为Term
的toPoly()
方法:
public Poly toPoly() {
// ...
for (Factor it : factors) {
poly = poly.mulPoly(it.toPoly());
}
if (sign == -1) {
poly.negate();
}
return poly;
}
negate()
方法可以将Poly
中的所有Mono
系数取反,以实现正负关系。
到此我们已经实现了绝大部分,最后我们只需有在顶层通过expr.toPoly()
获得最终表达式的多项式形式,再利用Poly
类中的toString()
方法输出最终展开后的结果。
输出优化
最后为了性能分更高,我们可以进行一定程度上的优化。例如省略一些可以去除的系数与指数,调整表达式各项顺序(使第一项为正)。第一次作业优化较为容易,经过这几步简单的优化就可以拿满性能分。
代码复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Expr.Expr() | 0 | 1 | 1 | 1 |
Expr.addTerm(Term) | 0 | 1 | 1 | 1 |
Expr.setExp(int) | 0 | 1 | 1 | 1 |
Expr.toPoly() | 8 | 3 | 5 | 5 |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.getNumber() | 2 | 1 | 3 | 3 |
Lexer.next() | 3 | 2 | 3 | 4 |
Lexer.peek() | 0 | 1 | 1 | 1 |
Mainclass.main(String[]) | 0 | 1 | 1 | 1 |
Mono.Mono(BigInteger, int, String) | 0 | 1 | 1 | 1 |
Mono.getCoe() | 0 | 1 | 1 | 1 |
Mono.getExp() | 0 | 1 | 1 | 1 |
Mono.getVarName() | 0 | 1 | 1 | 1 |
Mono.negate() | 0 | 1 | 1 | 1 |
Mono.plusCoe(BigInteger) | 0 | 1 | 1 | 1 |
Mono.toString() | 25 | 1 | 11 | 11 |
NumFactor.NumFactor(BigInteger) | 0 | 1 | 1 | 1 |
NumFactor.setExp(int) | 0 | 1 | 1 | 1 |
NumFactor.toPoly() | 0 | 1 | 1 | 1 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.parseExpr() | 7 | 1 | 6 | 6 |
Parser.parseFactor() | 7 | 3 | 5 | 5 |
Parser.parseTerm(int) | 4 | 1 | 4 | 4 |
Poly.Poly() | 0 | 1 | 1 | 1 |
Poly.addMono(Mono) | 0 | 1 | 1 | 1 |
Poly.addPoly(Poly) | 8 | 4 | 5 | 5 |
Poly.mergeSame(Poly) | 13 | 1 | 6 | 7 |
Poly.mulPoly(Poly) | 3 | 1 | 3 | 3 |
Poly.negate() | 1 | 1 | 2 | 2 |
Poly.toString() | 8 | 5 | 6 | 7 |
PowerFactor.PowerFactor(String, int) | 0 | 1 | 1 | 1 |
PowerFactor.setExp(int) | 0 | 1 | 1 | 1 |
PowerFactor.toPoly() | 0 | 1 | 1 | 1 |
PowerFactor.toString() | 0 | 1 | 1 | 1 |
Processor.Processor(String) | 16 | 4 | 6 | 8 |
Processor.getInput() | 0 | 1 | 1 | 1 |
Term.Term(int) | 0 | 1 | 1 | 1 |
Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
Term.reverseSign() | 0 | 1 | 1 | 1 |
Term.toPoly() | 2 | 1 | 3 | 3 |
Class | OCavg | OCmax | WMC |
---|---|---|---|
Expr | 2 | 5 | 8 |
Lexer | 2 | 4 | 8 |
Mainclass | 1 | 1 | 1 |
Mono | 2.43 | 11 | 17 |
NumFactor | 1 | 1 | 3 |
Parser | 3.75 | 5 | 15 |
Poly | 3.71 | 7 | 26 |
PowerFactor | 1 | 1 | 4 |
Processor | 4.5 | 8 | 9 |
Term | 1.5 | 3 | 6 |
Poly
类和Mono
类中的toString()
方法复杂度都偏高,由于需要考虑优化,不可避免需要大量分支进行特判,自然会产生高复杂度。Processor
类复杂度较高,在输入预处理时也需要特判不同的情况,来消除空白符和连续±号,复杂度高在所难免。Poly
类中的addPoly()
方法由于双重for
循环的存在(寻找同类项),导致复杂度增加。
第二次作业分析
UML类图
思路分析
自定义函数定义与解析
定义
首先新建FuncFactor
类,包含两个成员变量,newFunc
(函数实参替换形参后的字符串结果)与expr
(解析newFunc
后的结果)。
private String newFunc;
private Expr expr;
接着我建立了Definer
类来处理自定义函数定义与调用,我将Definer
中的成员变量和方法都设置成静态的,这样可以通过类名直接调用。
private static HashMap<String, String> funcMap = new HashMap<>();
private static HashMap<String, ArrayList<String>> paraMap = new HashMap<>();
// ...
public static void addFunc(String input) {...}
public static String callFunc(String name, ArrayList<Factor> actualParas) {...}
- 成员变量
funcMap
和paraMap
都是HashMap
类型,funcMap
可以将函数名与其对应函数定义式建立映射,paraMap
可以将函数名与其对应的形参列表建立映射。 addFunc()
方法将输入的函数表达式传入该函数并进行解析,并将该函数的函数定义式和形参列表加入funcMap
与paraMap
,callFunc()
方法通过遍历函数定义式,根据映射关系将其中的形参替换成实参的字符串形式(factor.toPoly().toString()
)。
解析
我在Parser
类中新增了parseFuncFactor()
方法(在parseFactor()
中调用),以解析自定义函数。
private Factor parseFuncFactor(String name) {
ArrayList<Factor> actualParas = new ArrayList<>();
actualParas.add(parseFactor());
while (!lexer.peek().equals(")")) {
// ...
}
return new FuncFactor(name, actualParas);
}
此时还需要对函数表达式进行解析,可以在FuncFactor
类的构造方法中实现。
public FuncFactor(String name, ArrayList<Factor> actualParas) {
this.newFunc = Definer.callFunc(name, actualParas);
this.expr = setExpr();
}
private Expr setExpr() {
Processor processor = new Processor(newFunc);// 对newFunc进行预处理
Lexer lexer = new Lexer(processor.getInput());
Parser parser = new Parser(lexer);
return parser.parseExpr();
}
自定义函数的处理告一段落。
指数函数解析
首先新建ExpFactor
类,类中有两个成员变量,factor
(指数函数内部因子)与exp
(指数函数幂次),相应也要增加toPoly()
方法。
private Factor factor;
private BigInteger exp;
在Parser
类中新增parseExpFactor()
方法来解析指数函数,该方法在parseFactor()
中被调用。当在parseFactor()
中发现当前解析因子为指数函数时,调用parseExpFactor()
方法,先解析指数函数内部因子(调用parseFactor()
方法),然后解析指数函数的幂次,最后返回一个ExpFactor
对象。
private Factor parseExpFactor() {
Factor inside = parseFactor();
lexer.next();
BigInteger exp = BigInteger.ONE;
if (lexer.peek().equals("^")) {
// ...
}
return new ExpFactor(inside, exp);
}
表达式展开
我的思路依旧是沿用第一次作业中的Mono
和Poly
形式,但这次作业多项式最小单元发生了改变,因此我改动了Mono
的数据组成,新增了成员变量poly
(指数函数内部因子调用toPoly()
方法后的结果)。
private BigInteger coe;
private BigInteger exp;
private Poly poly;
Poly
类的基本结构并无变化,但由于Mono
的变动,addPoly()
,mulPoly()
等方法需要进行微调,且需要重写euqals()
方法来判断两个Mono
中的poly
是否相同。
最后微调一下各个类中的toPoly()
方法,即可得到最终结果。
输出优化
在完成第二次作业时,由于与第一次作业跨度较大,我通过中测时已是周五,缺少充分的时间进行优化,因此仅仅进行了基本的合并同类项(更多的是怕优化出bug),甚至没有去除不必要的括号,这也导致了第二次作业性能分十分惨淡。
代码复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Definer.addFunc(String) | 8 | 3 | 6 | 7 |
Definer.callFunc(String, ArrayList) | 18 | 6 | 7 | 7 |
ExpFactor.ExpFactor(Factor, BigInteger) | 0 | 1 | 1 | 1 |
ExpFactor.setExp(BigInteger) | 0 | 1 | 1 | 1 |
ExpFactor.toPoly() | 2 | 2 | 2 | 2 |
Expr.Expr() | 0 | 1 | 1 | 1 |
Expr.addTerm(Term) | 0 | 1 | 1 | 1 |
Expr.setExp(BigInteger) | 0 | 1 | 1 | 1 |
Expr.toPoly() | 8 | 3 | 5 | 5 |
FuncFactor.FuncFactor(String, ArrayList) | 0 | 1 | 1 | 1 |
FuncFactor.setExp(BigInteger) | 0 | 1 | 1 | 1 |
FuncFactor.setExpr() | 0 | 1 | 1 | 1 |
FuncFactor.toPoly() | 0 | 1 | 1 | 1 |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.getNumber() | 2 | 1 | 3 | 3 |
Lexer.next() | 3 | 2 | 3 | 4 |
Lexer.peek() | 0 | 1 | 1 | 1 |
Mainclass.main(String[]) | 1 | 1 | 2 | 2 |
Mono.Mono(BigInteger, BigInteger, Poly) | 0 | 1 | 1 | 1 |
Mono.coeGreaterthanzero() | 8 | 1 | 5 | 5 |
Mono.coeLessthanzero() | 9 | 1 | 5 | 5 |
Mono.getCoe() | 0 | 1 | 1 | 1 |
Mono.getExp() | 0 | 1 | 1 | 1 |
Mono.getPoly() | 0 | 1 | 1 | 1 |
Mono.negate() | 0 | 1 | 1 | 1 |
Mono.plusCoe(BigInteger) | 0 | 1 | 1 | 1 |
Mono.toString() | 15 | 4 | 8 | 8 |
NumFactor.NumFactor(BigInteger) | 0 | 1 | 1 | 1 |
NumFactor.setExp(BigInteger) | 0 | 1 | 1 | 1 |
NumFactor.toPoly() | 0 | 1 | 1 | 1 |
NumFactor.toString() | 0 | 1 | 1 | 1 |
Output.Output(String) | 0 | 1 | 1 | 1 |
Output.strOut() | 1 | 1 | 2 | 2 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.parseExpFactor() | 1 | 1 | 2 | 2 |
Parser.parseExpr() | 7 | 1 | 6 | 6 |
Parser.parseFactor() | 16 | 6 | 12 | 12 |
Parser.parseFuncFactor(String) | 1 | 1 | 2 | 2 |
Parser.parseTerm(int) | 4 | 1 | 4 | 4 |
Poly.Poly() | 0 | 1 | 1 | 1 |
Poly.addMono(Mono) | 0 | 1 | 1 | 1 |
Poly.addPoly(Poly) | 12 | 4 | 7 | 7 |
Poly.equals(Poly, Poly) | 11 | 6 | 6 | 9 |
Poly.getMonoList() | 0 | 1 | 1 | 1 |
Poly.isEmpty() | 0 | 1 | 1 | 1 |
Poly.mergeSame(Poly) | 15 | 1 | 8 | 9 |
Poly.mulPoly(Poly) | 20 | 4 | 7 | 8 |
Poly.negate() | 1 | 1 | 2 | 2 |
Poly.toString() | 8 | 5 | 6 | 7 |
PowerFactor.PowerFactor(String, BigInteger) | 0 | 1 | 1 | 1 |
PowerFactor.setExp(BigInteger) | 0 | 1 | 1 | 1 |
PowerFactor.toPoly() | 0 | 1 | 1 | 1 |
Processor.Processor(String) | 16 | 4 | 6 | 8 |
Processor.getInput() | 0 | 1 | 1 | 1 |
Term.Term(int) | 0 | 1 | 1 | 1 |
Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
Term.reverseSign() | 0 | 1 | 1 | 1 |
Term.toPoly() | 2 | 1 | 3 | 3 |
Class | OCavg | OCmax | WMC |
---|---|---|---|
Definer | 7 | 7 | 14 |
ExpFactor | 1.33 | 2 | 4 |
Expr | 2 | 5 | 8 |
FuncFactor | 1 | 1 | 4 |
Lexer | 2 | 4 | 8 |
Mainclass | 2 | 2 | 2 |
Mono | 2.67 | 8 | 24 |
NumFactor | 1 | 1 | 4 |
Output | 1.5 | 2 | 3 |
Parser | 4 | 10 | 24 |
Poly | 4 | 8 | 40 |
PowerFactor | 1 | 1 | 3 |
Processor | 4.5 | 8 | 9 |
Term | 1.5 | 3 | 6 |
Poly
类和Mono
类中的toString()
方法复杂度都偏高,由于需要考虑优化,不可避免需要大量分支进行特判,自然会产生高复杂度。Processor
类复杂度较高,在输入预处理时也需要特判不同的情况,来消除空白符和连续±号,复杂度高在所难免。Poly
类中的addPoly()
方法和mulPoly()
方法由于需要比较各项Mono
,而Mono
中有含有poly
,递归层数会非常深,导致了高复杂度。- 由于新增了自定义函数和指数函数,
Parser
类中的parseFactor()
方法需要解析的因子数增多,提升了方法复杂度。
第三次作业分析
UML类图
思路分析
求导算子处理
第三次作业新增求导算子(自定义函数嵌套在我第二次作业的架构上已实现),首先我新建了DerFactor
类,该类内部有一个expr
成员变量,作为求导算子内部的表达式。
private Expr expr;
其次需要调整Parser
类,新增parseDerFactor()
方法(在parseFactor()
中被调用),用以解析求导算子。
private Factor parseDerFactor() {
Expr expr = parseExpr();
lexer.next();
return new DerFactor(expr);
}
最后就需要实现求导的计算了,简单思考可知,对求导算子内部表达式求导,其实可以理解为对内部表达toPoly()
的结果求导,即对该poly
内部一系列Mono
进行求导,根据求导法则即可对Mono
完成求导:
因此我在Poly
类中新增derPoly()
方法(本质还是递归),来实现求导计算,具体实现细节如下:
public Poly derPoly() {
Poly result = new Poly();
for (Mono mono : this.monoList) {
if (mono.getPoly().isEmpty()) {
if (mono.getExp().compareTo(BigInteger.ZERO) == 0) {
result.addMono(new Mono(BigInteger.ZERO, BigInteger.ONE, new Poly()));
} else {
// ...
}
} else {
if (mono.getExp().compareTo(BigInteger.ZERO) == 0) {
Poly tempploy1 = new Poly();
tempploy1.addMono(new Mono(mono.getCoe(), mono.getExp(), mono.getPoly()));
Poly tempploy2 = mono.getPoly().derPoly();
Poly newpoly = tempploy1.mulPoly(tempploy2);
for (Mono mono1 : newpoly.monoList) {
result.addMono(mono1);
}
} else {
// ...
}
}
}
return mergeSame(result);
}
即可完成第三次作业。
输出优化
第三次作业我进行了一定程度的优化,首先我去除了不必要的括号,其次若exp内部是一个单项式,则提出它的系数。
复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Definer.addFunc(String) | 8 | 3 | 6 | 7 |
Definer.callFunc(String, ArrayList) | 18 | 6 | 7 | 7 |
DerFactor.DerFactor(Expr) | 0 | 1 | 1 | 1 |
DerFactor.setExp(BigInteger) | 0 | 1 | 1 | 1 |
DerFactor.toPoly() | 0 | 1 | 1 | 1 |
ExpFactor.ExpFactor(Factor, BigInteger) | 0 | 1 | 1 | 1 |
ExpFactor.setExp(BigInteger) | 0 | 1 | 1 | 1 |
ExpFactor.toPoly() | 2 | 2 | 2 | 2 |
Expr.Expr() | 0 | 1 | 1 | 1 |
Expr.addTerm(Term) | 0 | 1 | 1 | 1 |
Expr.setExp(BigInteger) | 0 | 1 | 1 | 1 |
Expr.toPoly() | 8 | 3 | 5 | 5 |
Expr.toString() | 1 | 1 | 2 | 2 |
FuncFactor.FuncFactor(String, ArrayList) | 0 | 1 | 1 | 1 |
FuncFactor.setExp(BigInteger) | 0 | 1 | 1 | 1 |
FuncFactor.setExpr() | 0 | 1 | 1 | 1 |
FuncFactor.toPoly() | 0 | 1 | 1 | 1 |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.getNumber() | 2 | 1 | 3 | 3 |
Lexer.next() | 3 | 2 | 3 | 4 |
Lexer.peek() | 0 | 1 | 1 | 1 |
Mainclass.main(String[]) | 1 | 1 | 2 | 2 |
Mono.Mono(BigInteger, BigInteger, Poly) | 0 | 1 | 1 | 1 |
Mono.coeGreaterthanzero() | 8 | 1 | 5 | 5 |
Mono.coeLessthanzero() | 9 | 1 | 5 | 5 |
Mono.getCoe() | 0 | 1 | 1 | 1 |
Mono.getExp() | 0 | 1 | 1 | 1 |
Mono.getPoly() | 0 | 1 | 1 | 1 |
Mono.negate() | 0 | 1 | 1 | 1 |
Mono.plusCoe(BigInteger) | 0 | 1 | 1 | 1 |
Mono.toString() | 15 | 4 | 8 | 8 |
NumFactor.NumFactor(BigInteger) | 0 | 1 | 1 | 1 |
NumFactor.setExp(BigInteger) | 0 | 1 | 1 | 1 |
NumFactor.toPoly() | 0 | 1 | 1 | 1 |
NumFactor.toString() | 0 | 1 | 1 | 1 |
Output.Output(String) | 0 | 1 | 1 | 1 |
Output.strOut() | 1 | 1 | 2 | 2 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.parseDerFactor() | 0 | 1 | 1 | 1 |
Parser.parseExpFactor() | 1 | 1 | 2 | 2 |
Parser.parseExpr() | 7 | 1 | 6 | 6 |
Parser.parseFactor() | 17 | 7 | 13 | 13 |
Parser.parseFuncFactor(String) | 1 | 1 | 2 | 2 |
Parser.parseTerm(int) | 4 | 1 | 4 | 4 |
Poly.Poly() | 0 | 1 | 1 | 1 |
Poly.addMono(Mono) | 0 | 1 | 1 | 1 |
Poly.addPoly(Poly) | 12 | 4 | 7 | 7 |
Poly.derPoly() | 20 | 1 | 7 | 7 |
Poly.equals(Poly, Poly) | 11 | 6 | 6 | 9 |
Poly.getMonoList() | 0 | 1 | 1 | 1 |
Poly.isEmpty() | 0 | 1 | 1 | 1 |
Poly.mergeSame(Poly) | 15 | 1 | 8 | 9 |
Poly.mulPoly(Poly) | 20 | 4 | 7 | 8 |
Poly.negate() | 1 | 1 | 2 | 2 |
Poly.toString() | 8 | 5 | 6 | 7 |
PowerFactor.PowerFactor(String, BigInteger) | 0 | 1 | 1 | 1 |
PowerFactor.setExp(BigInteger) | 0 | 1 | 1 | 1 |
PowerFactor.toPoly() | 0 | 1 | 1 | 1 |
PowerFactor.toString() | 0 | 1 | 1 | 1 |
Processor.Processor(String) | 16 | 4 | 6 | 8 |
Processor.getInput() | 0 | 1 | 1 | 1 |
Term.Term(int) | 0 | 1 | 1 | 1 |
Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
Term.reverseSign() | 0 | 1 | 1 | 1 |
Term.toPoly() | 2 | 1 | 3 | 3 |
Term.toString() | 1 | 1 | 2 | 2 |
Class | OCavg | OCmax | WMC |
---|---|---|---|
Definer | 7 | 7 | 14 |
DerFactor | 1 | 1 | 3 |
ExpFactor | 1.33 | 2 | 4 |
Expr | 2 | 5 | 10 |
FuncFactor | 1 | 1 | 4 |
Lexer | 2 | 4 | 8 |
Mainclass | 2 | 2 | 2 |
Mono | 2.67 | 8 | 24 |
NumFactor | 1 | 1 | 4 |
Output | 1.5 | 2 | 3 |
Parser | 3.71 | 11 | 26 |
Poly | 4.27 | 8 | 47 |
PowerFactor | 1 | 1 | 4 |
Processor | 4.5 | 8 | 9 |
Term | 1.6 | 3 | 8 |
Poly
类和Mono
类中的toString()
方法复杂度都偏高,由于需要考虑优化,不可避免需要大量分支进行特判,自然会产生高复杂度。Processor
类复杂度较高,在输入预处理时也需要特判不同的情况,来消除空白符和连续±号,复杂度高在所难免。Poly
类中的addPoly()
方法和mulPoly()
方法由于需要比较各项Mono
,而Mono
中有含有poly
,递归层数会非常深,导致了高复杂度。- 由于新增了求导算子,
Parser
类中的parseFactor()
方法需要解析的因子数增多,提升了方法复杂度。 Definer
类中的callFunc()
方法,由于此时表达式复杂度达到了顶峰,需要替换的自定义函数数量增多,提升了复杂度。Poly
类中的equals()
方法和derPoly()
方法,由于递归层数很深,导致了高复杂度。
自己的bug
第一次作业
在写第一次作业时,前两三天我感到非常力不从心,递归下降的复杂让我无暇顾及是否有bug,只能憋着一口气往下写。在完工之后,随便捏了几个数据就发现了一堆bug。
首先在逻辑层面,我的Poly
类中的addPoly()
方法和mulPoly()
方法写的有问题,不过改动比较小,很快就修复了。
其次输出层面,我的toString()
方法少判断了一种情况,导致所有的-1
都输出为1
,但发现这个bug之后也很快就修复了。
第一次作业的强测也是得到了100分,算是开了个好头。
第二次作业
第二次作业的bug就比较多了,在编写完代码之后,我用同学的自动评测机测试了自己的程序,主要有以下两个问题:
1、当自定义函数内部因子为一个带符号常数因子时(例如+2
,-3
),我的程序会报错,报错信息集中在Parser
类中。
2、当测试点比较复杂时,计算结果会有不同(出现频率大概是十分之一)。
第一个问题较容易定位,经过对Parser
类的分析,我在处理parseFuncFactor()
时忽略了常数因子会带有符号的可能,加入分支判断后解决了这个问题。第二个问题就很难定位了,出错的测试点不具有明显的相似性,更像是一个逻辑层面的错误。毫无头绪的我开始翻阅去年oopre课的资料,在看到深克隆与浅克隆时我突然浑身一震,进而茅塞顿开。在mulPoly()
方法中会产生新的Mono
。但按照我之前的写法所有新产生的Mono
都是浅克隆得到的,因此会出现bug,于是我对所有不确定应该采用深克隆还是浅克隆的地方一律改成了深克隆,虽然可能会多占用一定内存,但能保证不出错。其次我对equals()
方法也进行了修正(之前只比较了两个Mono
内部的poly
是否互相包含,但忘记比较两个poly
的Mono
数量),改正之后顺利通过了中测。
强测情况:得分89.9分,强测点2显示tle((((((((((((x^8)^8)^8)^8)^8)^8)^8)^8)^8)^8)^8)^8
),经过长时间的断点调试,我发现了原因。在所有mulPoly()
方法中产生的Mono
,我都将其加入了它应属于的Poly
,但若该Mono
系数为0,在下次进行多项式乘法运算时就会产生更多的系数为0的Mono
,这种增长是指数级的。因此当嵌套幂次层数增加时,就会因为Mono
项数过多导致tle。于是我在addPoly()
方法和mulPoly()
方法中加入分支特判,如果系数为0则不加入Poly
,顺利修复了bug。
第三次作业
第三次作业的变化与第二次作业相差不大,因此在修复了第二次作业bug之后,第三次作业基本没有出现bug,最终强测得到了96分,扣除的分数集中在性能分损失上。
发现别人bug的策略
在三次互测中我都没有发现房友的bug(鉴定为oo小绵羊),我寻找房友bug的主要策略是手动捏造数据+评测机测试。
后者效率不是很高,由于我三次互测都在A房,大家的程序都经过了不止一个评测机的大规模测试,因此仅仅通过评测机是很难发现bug的,相比之下手动构造数据更容易发现bug。在前两次互测时通过阅读房友的代码,我发现部分代码递归层数会更深,因此我尝试构造会导致深递归层数的测试点,成功使两个房友的程序tle了,但由于cost的限制,这些测试点无法作为互测点提交,我无法构造出满足cost并使房友程序tle的测试点,遂放弃。
第三次互测我的舍友成功构造出了满足cost且会导致tle的测试点(貌似是因为部分同学程序会使toString()
方法调用过多,具体细节我没有考虑),并成功hack了他的房友。遗憾的是我的房友都不会出现该问题,最后也是没有发现其他bug。
心得体会
总的感受可以用两个词概括:痛苦、满足(我不是m)
痛苦的是思考架构的时候,写代码的时候,debug的时候;满足的是通过公测、强测的时候。虽然痛苦的时间可能占到了99%,但那1%的满足感实在令人着迷。而且无论是痛苦还是满足,都让我真真切切的感受到我是一个活着的人,我有着强盛的生命力,这种感受无与伦比。
在写第一次作业的时候,在思考了两天还没有写下一行代码的时候,我一直问自己真的能做到吗,我真的可以完成吗,沮丧、懊恼,甚至萌生了转专业的想法。但时间过得真快啊,转眼oo第一单元已经结束了,我真的做到了,虽然强测仍有遗憾,但我已经做到我力所能及的最好了。我感受到了自己面向对象思维的提升,也学会了递归下降算法的使用以及层次化的设计,为第二单元多线程学习做好了准备。
感谢oo带给我的提升,重整行囊,继续出发!
抒情完聊点干货。
- 认识到了架构的重要性,倘若第一次作业选择了正则表达式,那么肯定是无法完成后两次作业的,必然会经历重构,这体现了递归下降架构的优势,好的架构真的会起到事半功倍的效果。
- 认识到了测试的重要性,一个不经过边界测试、压力测试的程序有很大可能无法在强测中取得高分,一定要充分测试(Junit、评测机、手动构造)。
- 讨论区也有很大帮助,它可能在你灵感枯竭的时候拯救你。
- 思路尽量发散一些,注意代码的可扩展性,提前思考下次作业可能会出现哪些迭代需求,做好准备。
未来方向
- oo第一单元整体上的设计还是很合理的,层层递进。但第三次作业的体量相比前两次明显降低(也可能是大家的能力得到了提升),可以综合一下三次作业的规模,使任务量更合理。
- 可以在性能分的评比中加入运行时间。
致谢
十分感谢Hyggge学长的博客,可以说是救了我的命,感谢学长架构!
十分感谢cxc同学的强大评测机,我找出了不少bug。