前言
本次面向对象设计与构造第一单元的三次作业通过迭代实现了对输入的表达式进行展开和计算的功能。通过本单元学习,我对于递归下降和深克隆等等有了更深的认识。
架构设计体验
第一次作业
本次作业需要完成的任务为:读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号后的表达式。
第一次作业的要求不算复杂,最大的难点在于理解课程组给出的关于表达式、项和因子的层次化表达以及递归下降的方法。在我看来第一次作业可以拆解成两大模块:表达式的解析、展开后的计算和合并。
解析这部分的核心就是Lexer和Parser两个类,Lexer作为分词器负责将输入的表达式的组成部分识别和提取出来,在这里我借鉴了oolens公众号推文中的做法,在初始化Lexer时就将输入的字符串input解析好存入ArrayList<Token>
中,之后在Parser中每次调用Lexer.nextToken()
方法将ArrayList的下标加一即可。
//Lexer.java
public class Lexer{
private int pos = 0;
private final String input;
private int indexOfToken = 0;
private ArrayList<Token> tokens;
public Lexer(String input) {
//TODO
}
public void nextToken(){
indexOfToken++;
}
public Token curToken(){
return tokens.get(indexOfToken);
}
}
Parser将我们拆分好的token解析成对应的因子、项和表达式。这部分给我带来的最大的收获就是“铁路警察,各管一段”的思想,当parseExpr()的时候需要得到构成这个Expr的各个项,但是我们只需要直接调用parseTerm()就好,不必去考虑parseTerm()实现的具体细节,同理当parseTerm()的时候也不必去思考parseFactor()是怎么实现的,真正使得各个方法各司其职。
而到了展开计算这部分,我借鉴了往届学长博客的思路,因为我们最后得到的结果都是形如
∑
a
∗
x
b
\sum{a*x^b}
∑a∗xb
这样的形式,所以我用Unit类来存储a*x^b这样的单项式,用Poly类来存储求和后的多项式,在Poly类中设置addPoly()和mulPoly()进行加法乘法计算,对应的在Unit中也要实现mulUnit()方法。
//Unit.java
public class Unit{
private BigInteger coe; //系数
private BigInteger expo; //x的指数
public Unit mulUnit(Unit other) {
return new Unit(coe.multiply(other.coe), expo.multiply(other.expo));
}
}
第二次作业
本次作业中需要完成的任务为:读入一系列自定义函数的定义以及一个包含幂函数、指数函数、自定义函数调用的表达式,输出恒等变形展开所有括号后的表达式。相较于第一次作业改变比较大,尤其体现在最后多项式的加法乘法以及合并同类项的逻辑上。
自定义函数部分
在自定义函数部分我设计了两个类,CustomDefiner和FuncFactor类,分别负责处理自定义函数以及在解析的时候转化为函数因子。
CustomDefiner
由于我们定义的函数并不为某一个因子所特有,而是在这一表达式中通用,联想到课上荣文戈老师举出的选课系统的例子加上往届学长的优秀博客的启发,我打算采用一个静态的工具类来存储自定义函数的相关信息,如函数名、函数的形参以及函数的定义式,并且它并不实例化对象,所以所有的属性和方法都设置成static。
在该类中包含两个HashMap属性:
- func2def通过函数名寻找到函数的定义式
- func2virtual通过函数名寻找到形参列表。
public class CustomDefiner {
private static HashMap<String, String> func2def = new HashMap<>();
private static HashMap<String, ArrayList<String>> func2virtual = new HashMap<>();
}
同时该类包含两个方法:
- addFunc负责在标准输入自定义函数中将函数相关信息进行解析和存储。
- callFunc负责解析表达式中的函数因子时将实参代入进行表达式替换
//CustomDefiner.java
public static void addFunc(String input){
//TODO//
}
public static String callFunc(String name, ArrayList<Factor> actualParameters){
//TODO//
}
值得一提的是,在callFunc中进行表达式替换时我并没有采取replaceAll的方式,因为这样可能导致字母替换的混乱,所以在这里我采取了一种朴素的略显笨拙的方案,就是建立一个从形参到实参的HashMap映射,然而遍历函数的定义式字符串,如果遇到形参就替换成实参,其中遇到exp的时候要特殊处理,具体实现如下:
//CustomDefiner.java
//callFunc
StringBuilder sb = new StringBuilder();
HashMap<String, Factor> vir2actual = new HashMap<>();
for (int i = 0; i < parameters.size(); i++) {
vir2actual.put(parameters.get(i), actualParameters.get(i));
}
for (int i = 0; i < definition.length(); i++) {
String cur = String.valueOf(definition.charAt(i));
if(cur.equals("e")) {
sb.append("exp(");
i+=3;
}
else if (vir2actual.containsKey(cur)) {
Factor factor = vir2actual.get(cur);
sb.append(factor.toPoly().toString());
} else {
sb.append(cur);
}
}
return sb.toString();
FuncFactor
在FuncFactor类中只包含一个Expr属性funcExpr, 意为将表达式中的函数替换后得到的表达式:
//FuncFactor.java
public class FuncFactor implements Factor {
private Expr funcExpr;
public FuncFactor(String name, ArrayList<Factor> actualParameters) {
String funcString = CustomDefiner.callFunc(name,actualParameters);
Lexer lexer = new Lexer(PreProcess.process(funcString));
Parser parser = new Parser(lexer);
funcExpr = parser.parseExpr();
}
指数函数和合并同类项
指数函数因子的实现比较简单,和第一次作业区别不大,这里不再赘述。
在我看来本次作业最大的难点在于最后答案的形式发生很大的变化,每一个最小单元加入了exp(factor)^n,最后的形式变成了
∑
a
∗
x
b
∗
e
e
x
p
E
x
p
o
\sum {a*x^b*e^{expExpo}}
∑a∗xb∗eexpExpo
因此加法乘法和合并同类项的逻辑需要一些变动:
Unit类
-
Unit类中,首先加入了指数函数括号外的指数expExpo以及exp括号内的多项式inside,为了方便运算,直接把inside定义为Poly类型的。
//Unit.java public class Unit { private BigInteger coe; private BigInteger expo; private Poly inside; private BigInteger expExpo; }
-
在运算时涉及到了深克隆的问题,如果直接在原对象上修改会产生一些意料之外的后果,于是我添加了一个深克隆方法
//Unit.java public Unit deepClone() { BigInteger newCoe = new BigInteger(coe.toString()); if (inside.Empty() || inside.isZero() || expExpo.compareTo(zero) == 0) { return new Unit(newCoe, expo); } else { Poly newInside = inside.deepClone(); BigInteger newExpExpo = new BigInteger(expExpo.toString()); return new Unit(newCoe, expo, newInside, newExpExpo); } }
-
由于要判断是否可以合并同类项,这里我重写了equals方法进行判断
Poly类
-
这里很多同学采用的HashMap来存储Unit来快速地进行合并同类项,我在采用HashMap的时候出现了很多问题,最后才用了ArrayList来存储,通过双重for循环来合并同类项。
-
在Poly类中同样需要实现深克隆,由于Unit中inside为Poly类型,所以深克隆的时候进行递归的调用
//Poly.java public Poly deepClone() { Poly poly = new Poly(); for (Unit unit : this.units) { Unit newUnit = unit.deepClone(); poly.addUnit(newUnit); } return poly; }
第三次作业
本次作业中需要完成的任务为:读入一系列自定义函数的定义以及一个包含幂函数、指数函数、自定义函数调用、求导算子的表达式,输出恒等变形展开所有括号后的表达式。
在前两次作业架构的基础上,第三次作业实现起来比较容易,在求导这一问题上,我认为有两种方法,一种是层次化求导,也就是在Expr, Term和Factor类中实现derive()方法,遇到dx()就调用对应的derive();另一种是在最后转换成多项式后再求导也就是在Unit和Poly中实现derive()方法。这里我选择的是前者。
层次化求导的derive()方法比较好实现,为了解析求导因子,我建了个Derivation类来存储求导信息,其中要注意的是当求导因子嵌套的时候,要将Derivation存储的表达式转换成字符串,再用lexer和parser重新解析一次。
//Derivation.java
public class Derivation implements Factor {
private Expr expr;
private Poly result;
public Derivation(Expr expr) {
this.expr = expr;
this.result = expr.derive();
}
@Override
public Poly toPoly() {
return result;
}
@Override
public Poly derive() {
Lexer lexer = new Lexer(this.toPoly().toString());
Parser parser = new Parser(lexer);
return parser.parseExpr().derive();
}
@Override
public Factor deepClone() {
return new Derivation(this.expr.deepClone());
}
}
新的迭代场景
- 如果加入新的因子,可以直接实现Factor的接口,然而修改Unit中的属性、输出逻辑以及Poly类中的加法乘法即可
- 如果加入新的变量,例如x, y, z等等,只需修改Unit中的toString()方法即可
bug分析
我的bug主要集中在第一次作业上,有的项在运算后系数会变为0,我在toString()时将其变为空串,但是在最后输出的时候忘记特判使得加号保留了下来。第二次和第三次没有出现正确性上的bug,但是会有没有合并同类项的情况导致性能分暴跌,我的设计中由于用ArrayList存储Unit,这就导致exp((x^2+2*x))
和exp((2*x+x^2))
无法合并掉。
在互测中我利用评测机对房间内的成员的代码评测,大致确定bug产生的原因,然后针对这个bug手动构造一些复杂度较低但是精确hack的数据。找到的bug大多是在优化输出长度的过程中产生的,例如寻找最大公因数的时候出现除0操作,还有形如exp(-x^2)这样的去掉括号过多导致输出不合法。
优化方面
考虑到性能分占比不大,而且在优化exp的时候要考虑到的情况比较复杂,所以第二次作业我没有进行太多优化,仅仅特判了一下exp内是数字的情况,例如exp(2)^2–>exp(4)
,但是强测中有两个点的性能分直接爆零,其中一个测试点是exp内的多项式每一项的因数都相同,可以提取出去。所以在第三次作业中我特别处理了这种情况,以及优化了exp((4*x))–>exp(x)^4
这类情况。
但是优化方面我在toString()中用到了很多if-else
来进行特判,导致代码的简洁性和可读性不高,目前还没想到太好的解决方案,课后计划和同学探讨一下。
程序结构度量
第三次作业UML图
代码规模:
复杂度分析
方法复杂度:
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
CustomDefiner.addFunc(String) | 3.0 | 1.0 | 3.0 | 3.0 |
CustomDefiner.callFunc(String, ArrayList) | 7.0 | 3.0 | 5.0 | 5.0 |
Derivation.deepClone() | 0.0 | 1.0 | 1.0 | 1.0 |
Derivation.Derivation(Expr) | 0.0 | 1.0 | 1.0 | 1.0 |
Derivation.derive() | 0.0 | 1.0 | 1.0 | 1.0 |
Derivation.toPoly() | 0.0 | 1.0 | 1.0 | 1.0 |
ExpFactor.deepClone() | 0.0 | 1.0 | 1.0 | 1.0 |
ExpFactor.derive() | 0.0 | 1.0 | 1.0 | 1.0 |
ExpFactor.ExpFactor(Factor, BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
ExpFactor.toPoly() | 0.0 | 1.0 | 1.0 | 1.0 |
ExpFactor.toString() | 1.0 | 2.0 | 1.0 | 2.0 |
Expr.addTerm(Term) | 0.0 | 1.0 | 1.0 | 1.0 |
Expr.deepClone() | 1.0 | 1.0 | 2.0 | 2.0 |
Expr.derive() | 14.0 | 1.0 | 7.0 | 7.0 |
Expr.Expr() | 0.0 | 1.0 | 1.0 | 1.0 |
Expr.setExpo(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Expr.toPoly() | 6.0 | 2.0 | 4.0 | 4.0 |
Expr.toString() | 1.0 | 1.0 | 2.0 | 2.0 |
FuncFactor.deepClone() | 0.0 | 1.0 | 1.0 | 1.0 |
FuncFactor.derive() | 0.0 | 1.0 | 1.0 | 1.0 |
FuncFactor.FuncFactor(String, ArrayList) | 0.0 | 1.0 | 1.0 | 1.0 |
FuncFactor.FuncFactor(String, Expr) | 0.0 | 1.0 | 1.0 | 1.0 |
FuncFactor.toPoly() | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.back() | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.curToken() | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.getNumber() | 2.0 | 1.0 | 3.0 | 3.0 |
Lexer.haveNext() | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.Lexer(String) | 15.0 | 1.0 | 14.0 | 16.0 |
Lexer.nextToken() | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.notEnd() | 0.0 | 1.0 | 1.0 | 1.0 |
Main.main(String[]) | 0.0 | 1.0 | 1.0 | 1.0 |
MyScan.MyScan(Scanner) | 0.0 | 1.0 | 1.0 | 1.0 |
MyScan.solve() | 1.0 | 1.0 | 2.0 | 2.0 |
Number.deepClone() | 0.0 | 1.0 | 1.0 | 1.0 |
Number.derive() | 0.0 | 1.0 | 1.0 | 1.0 |
Number.getValue() | 0.0 | 1.0 | 1.0 | 1.0 |
Number.Number(String) | 0.0 | 1.0 | 1.0 | 1.0 |
Number.toPoly() | 0.0 | 1.0 | 1.0 | 1.0 |
Number.toString() | 0.0 | 1.0 | 1.0 | 1.0 |
Parser.parseDerive() | 0.0 | 1.0 | 1.0 | 1.0 |
Parser.parseExpFactor() | 7.0 | 3.0 | 4.0 | 5.0 |
Parser.parseExpo() | 5.0 | 1.0 | 4.0 | 4.0 |
Parser.parseExpr() | 8.0 | 1.0 | 7.0 | 7.0 |
Parser.parseFactor() | 22.0 | 8.0 | 12.0 | 12.0 |
Parser.parseFuncFactor(String) | 5.0 | 1.0 | 4.0 | 4.0 |
Parser.Parser(Lexer) | 0.0 | 1.0 | 1.0 | 1.0 |
Parser.parseTerm(int) | 2.0 | 1.0 | 3.0 | 3.0 |
Poly.addPoly(Poly) | 2.0 | 1.0 | 3.0 | 3.0 |
Poly.addUnit(Unit) | 0.0 | 1.0 | 1.0 | 1.0 |
Poly.deepClone() | 1.0 | 1.0 | 2.0 | 2.0 |
Poly.divideCoe() | 1.0 | 1.0 | 2.0 | 2.0 |
Poly.Empty() | 0.0 | 1.0 | 1.0 | 1.0 |
Poly.equals(Object) | 1.0 | 1.0 | 2.0 | 2.0 |
Poly.getSameCoe() | 0.0 | 1.0 | 1.0 | 1.0 |
Poly.getUnits() | 0.0 | 1.0 | 1.0 | 1.0 |
Poly.hashCode() | 0.0 | 1.0 | 1.0 | 1.0 |
Poly.hasNoExp() | 1.0 | 2.0 | 2.0 | 2.0 |
Poly.hasOneFactor() | 1.0 | 2.0 | 2.0 | 2.0 |
Poly.hasOnlyNumber() | 1.0 | 1.0 | 2.0 | 2.0 |
Poly.haveSameCoe() | 3.0 | 3.0 | 2.0 | 3.0 |
Poly.isZero() | 1.0 | 1.0 | 2.0 | 2.0 |
Poly.mergePoly() | 8.0 | 1.0 | 5.0 | 5.0 |
Poly.mulPoly(Poly) | 3.0 | 1.0 | 3.0 | 3.0 |
Poly.Poly() | 0.0 | 1.0 | 1.0 | 1.0 |
Poly.toString() | 14.0 | 6.0 | 6.0 | 8.0 |
Power.deepClone() | 0.0 | 1.0 | 1.0 | 1.0 |
Power.derive() | 0.0 | 1.0 | 1.0 | 1.0 |
Power.Power(String, BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Power.toPoly() | 0.0 | 1.0 | 1.0 | 1.0 |
Power.toString() | 3.0 | 3.0 | 2.0 | 3.0 |
PreProcess.process(String) | 21.0 | 1.0 | 7.0 | 10.0 |
Term.addFactor(Factor) | 0.0 | 1.0 | 1.0 | 1.0 |
Term.deepClone() | 1.0 | 1.0 | 2.0 | 2.0 |
Term.derive() | 6.0 | 1.0 | 4.0 | 4.0 |
Term.Term(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Term.toPoly() | 1.0 | 1.0 | 2.0 | 2.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 |
Unit.deepClone() | 3.0 | 2.0 | 4.0 | 4.0 |
Unit.equals(Object) | 2.0 | 1.0 | 4.0 | 4.0 |
Unit.getCoe() | 0.0 | 1.0 | 1.0 | 1.0 |
Unit.getExpo() | 0.0 | 1.0 | 1.0 | 1.0 |
Unit.getInsideMultiExpo() | 0.0 | 1.0 | 1.0 | 1.0 |
Unit.hasNoExp() | 1.0 | 1.0 | 3.0 | 3.0 |
Unit.hasOneFactor() | 5.0 | 3.0 | 4.0 | 6.0 |
Unit.hasOnlyNumber() | 1.0 | 1.0 | 2.0 | 2.0 |
Unit.haveSameExpo(Unit) | 0.0 | 1.0 | 1.0 | 1.0 |
Unit.isNegCoe() | 0.0 | 1.0 | 1.0 | 1.0 |
Unit.mulUnit(Unit) | 8.0 | 4.0 | 3.0 | 4.0 |
Unit.optimise(StringBuilder) | 6.0 | 1.0 | 4.0 | 4.0 |
Unit.setCoe(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Unit.setEmpty() | 0.0 | 1.0 | 1.0 | 1.0 |
Unit.toString() | 42.0 | 3.0 | 22.0 | 23.0 |
Unit.Unit(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Unit.Unit(BigInteger, BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Unit.Unit(BigInteger, BigInteger, Poly, BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
Total | 237.0 | 129.0 | 229.0 | 244.0 |
Average | 2.443298969072165 | 1.3298969072164948 | 2.3608247422680413 | 2.515463917525773 |
其中由于输出逻辑比较复杂,导致Unit.toString()的复杂度比较高
类复杂度: |
class | OCavg | OCmax | WMC |
---|---|---|---|
CustomDefiner | 4.0 | 5.0 | 8.0 |
Derivation | 1.0 | 1.0 | 4.0 |
ExpFactor | 1.2 | 2.0 | 6.0 |
Expr | 2.5714285714285716 | 7.0 | 18.0 |
FuncFactor | 1.0 | 1.0 | 5.0 |
Lexer | 3.0 | 14.0 | 21.0 |
Main | 1.0 | 1.0 | 1.0 |
MyScan | 1.5 | 2.0 | 3.0 |
Number | 1.0 | 1.0 | 6.0 |
Parser | 3.875 | 11.0 | 31.0 |
Poly | 2.1666666666666665 | 8.0 | 39.0 |
Power | 1.4 | 3.0 | 7.0 |
PreProcess | 8.0 | 8.0 | 8.0 |
Term | 2.0 | 4.0 | 10.0 |
Token | 1.0 | 1.0 | 3.0 |
Token.Type | 0.0 | ||
Unit | 2.3333333333333335 | 17.0 | 42.0 |
Total | 212.0 | ||
Average | 2.185567010309278 | 5.375 | 12.470588235294118 |
心得体会
有了上学期oopre学习的java语法和面向对象理念的基础,加上公众号和实验中对于递归下降的训练,第一单元的作业相对来说不是特别困难。我的收获主要是以下几点:
- 使用递归下降的方法逻辑清晰,实现简单,同时”铁路警察,各管一段“的理念和面向对象的设计方式更加贴合
- 学会利用工具类来处理类之间共用的属性(PreProcess, CustomDefiner)
- 使用深克隆来避免在同一对象上进行错误修改
- 单独使用一个类来进行输入(MyScan)
- 在互测中找到其他同学bug以及修复自身bug来提升面向对象的能力
未来方向
建议课程组可以提供一些简单的编程练习题来训练Lexer和Parser之类的用法,第一次上手接触递归下降还是比较陌生。