文章目录
第一单元:表达式分析
第一次作业分析
UML
任务分析
- 读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号后的表达式。
我是这样思考的:
展开括号
⟹
怎么展
⟹
要知道括号里有什么,括号外有什么
⟹
要获取各个字符
⟹
根据运算规则去括号
展开括号\Longrightarrow怎么展\Longrightarrow要知道括号里有什么,括号外有什么\Longrightarrow要获取各个字符\Longrightarrow根据运算规则去括号
展开括号⟹怎么展⟹要知道括号里有什么,括号外有什么⟹要获取各个字符⟹根据运算规则去括号
根据给定的规则,字符之间有很明显的层级关系:最小的计算单元是因子,因子通过乘法运算组成项,项通过加减运算构成表达式,同时 '(' 表达式 ')' [空白项 指数]
构成了表达式因子——出现了递归嵌套的情形。
使用递归下降算法可以有效解决嵌套的问题,同时减少代码量,简化计算逻辑。通过递归下降我们就能获取表达式所包含的所有信息——哪些对象参与运算、它们的运算逻辑是什么。
之后便是展开括号的问题。有多种方式可以实现我们想要的结果,我主要参考 Hyggge 学长这篇博客的方式。此后的整体架构也大体参照学长的架构。
解析任务——递归下降分析
递归下降包括两部分:Lexer
(词法分析器)和Parser
(解析器),词法分析器将复杂的表达式分解为最小的语法单元,而解析器则是将词法分析器提供的语法单元按照运算要求组合在一起,让输入的 String 类型的表达式在计算机的存储里真正具有运算意义。
Lexer
我将语法单元分成如下几类:
// Token.java
public enum Type { // 按运算优先级从低到高
NULL, // 初始状态
ADD, SUB, // expr.Expr -> Term
MUL, // Term -> expr.Factor
NUM, VAR, EXP, LP, RP; // expr.Factor
}
通过遍历字符串来获取每个语法单元,并将语法单元的类型和内容放到 Token
里
Parser
解析表达式,就是将表达式解析为 项 + 项 + 项 ......
的形式,所以需要获取项,而解析项就是获取因子,所以这样组织代码:
// Parse.java
......
......
public Expr parseExpr() {
Expr expr = new Expr();
int sign = 1;
if (lexer.hasNext() && lexer.peek().getType() == Token.Type.SUB) { // 第一项前有 -
sign = -1;
lexer.nextToken();
} else if (lexer.hasNext() && lexer.peek().getType() == Token.Type.ADD) { // 第一项前有 +
lexer.nextToken();
}
expr.addTerm(parseTerm(sign));
while (lexer.hasNext() && (lexer.peek().getType() == Token.Type.ADD ||
lexer.peek().getType() == Token.Type.SUB)) {
if (lexer.peek().getType() == Token.Type.SUB) {
lexer.nextToken(); // 跳过 -
expr.addTerm(parseTerm(-1));
} else {
lexer.nextToken(); // 跳过 +
expr.addTerm(parseTerm(1));
}
}
return expr;
}
public Term parseTerm(int sign) { // sign 是整个项的符号
......
}
public Factor parseFactor() {
if (lexer.peek().getType() == Token.Type.LP) { // 表达式因子 ......}
else if (lexer.peek().getType() == Token.Type.VAR) { // 幂函数因子......}
else { // 常数因子
if (lexer.peek().getType() == Token.Type.ADD || // 有符号
lexer.peek().getType() == Token.Type.SUB) {......}
else { // 无符号......}
}
}
public String parseIndex() { // 从 ^ 开始解析
// 解析表达式因子和幂函数因子可能存在的指数
......
}
展开任务——去括号
我们可以这样考虑,如果已经完成了展开括号,那么最后的表达式应该是这样的:
E
x
p
r
=
∑
i
n
a
i
x
n
i
Expr = \sum_i^n a_ix^{n_i}
Expr=i∑naixni
我们的任务就是将解析后的语法单元集合等价转化为以上多项式,也就有这样的对应规则:
E
x
p
r
⟹
P
o
l
y
(
多项式
)
Expr \Longrightarrow Poly (多项式)
Expr⟹Poly(多项式)
所以新建两个类: Poly
类和 Mono
类,它们的关系是:
M
o
n
o
=
a
x
n
Mono = ax^n
Mono=axn
P o l y = ∑ M o n o Poly = \sum Mono Poly=∑Mono
对于 Mono
:
// Mono.java
......
private BigInteger coe; // 底数 -> a
private int exp; // 指数 ->n
public Mono(BigInteger coe, int exp) {
this.coe = coe;
this.exp = exp;
}
......
对于 Poly
:
// Poly.java
......
private ArrayList<Mono> monoList;
private HashMap<Integer, BigInteger> monoMap; // key->Mono的指数 | value->Mono的底数
public Poly() {
this.monoList = new ArrayList<>();
this.monoMap = new HashMap<>();
}
public Poly(BigInteger coe, int exp) { // 用 Mono 创建一个 Poly
this.monoList = new ArrayList<>();
this.monoMap = new HashMap<>();
monoMap.put(exp, coe);
monoList.add(new Mono(coe, exp));
}
......
将 Expr
变为多项式的过程,也就是去括号的过程。Expr
转换为多项式,相当于组成它的所有 Term
转换为多项式再求和;而 Term
转换为多项式相当于组成它的所有 Factor
转换为多项式再累积。
E
x
p
r
.
t
o
P
o
l
y
=
∑
T
e
r
m
.
t
o
P
o
l
y
Expr.toPoly = \sum Term.toPoly
Expr.toPoly=∑Term.toPoly
T e r m . t o P o l y = ∏ F a c t o r . t o P o l y Term.toPoly = \prod Factor.toPoly Term.toPoly=∏Factor.toPoly
对于其它 Factor.toPoly
:
// Num.java
public Poly toPoly() {
return new Poly(new BigInteger(num),0);
}
// Power.java
public Poly toPoly() {
return new Poly(new BigInteger("1"), Integer.parseInt(exp));
}
由于在 Poly
类中采用了 指数 -> 底数
的存储方式,所以在展开括号时同时实现化简操作,确保相同指数的幂函数均被合并。
// Mono.java
// 两个 Mono 能否相加
public boolean canAdd(Mono){......}
// 两个 Mono相加,注意返回新的 Mono,而不是修改 this 或者传入的 Mono
public Mono add(Mono){......}
// 两个 Mono相乘,注意返回新的 Mono,而不是修改 this 或者传入的 Mono
public Mono mul(Mono){......}
// Poly.java
public Poly addPoly(Poly poly) { // 多项式相加,返回相加后的新的多项式
Poly newPoly = new Poly();
HashMap<Integer, BigInteger> tmpMap = poly.getMonoMap(); // 可能为 null
for (Integer expThis : monoMap.keySet()) {
if (tmpMap.containsKey(expThis)) {......}
else {......}
}
for (Integer expOut : tmpMap.keySet()) {......}
return newPoly;
}
public Poly mulPoly(Poly poly) { // 多项式相乘,返回相乘后新的多项式
Poly newPoly = new Poly();
for (Integer expThis : monoMap.keySet()) { // 指数集合
HashMap<Integer, BigInteger> tmpMap = poly.getMonoMap();
for (Integer expOut : tmpMap.keySet()) {......} // addPoly
}
return newPoly;
}
public Poly powPoly(int exp) { // () ^ exp 展开
Poly newPoly = new Poly(new BigInteger("1"), 0);
for (int i = 0; i < exp; ++i) {......} // mulPoly
return newPoly;
}
优化任务
对于 ax^n
中 a 和 n 取特殊值的优化,比如 1 | -1 | 0
Bug 修复
在优化 Term 时没有判断 string.length()
是否大于 0,然后使用了 string.charAt(0)
导致了bug
复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Adjust.parseCoefficient(String) | 1 | 2 | 1 | 2 |
Adjust.simplifyPoly(String) | 10 | 3 | 6 | 7 |
Adjust.simplifyString(String) | 0 | 1 | 1 | 1 |
Adjust.simplifyTerm(BigInteger, int) | 9 | 8 | 6 | 8 |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.getNumber() | 2 | 1 | 3 | 3 |
Lexer.hasNext() | 0 | 1 | 1 | 1 |
Lexer.nextToken() | 5 | 2 | 2 | 9 |
Lexer.peek() | 0 | 1 | 1 | 1 |
Main.main(String[]) | 0 | 1 | 1 | 1 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Parser.parseExpr() | **10** | 1 | 9 | 9 |
Parser.parseFactor() | 7 | 4 | 5 | 5 |
Parser.parseIndex() | 5 | 2 | 4 | 4 |
Parser.parseTerm(int) | 6 | 1 | 7 | 7 |
Token.Token(Type, String) | 0 | 1 | 1 | 1 |
Token.getContent() | 0 | 1 | 1 | 1 |
Token.getType() | 0 | 1 | 1 | 1 |
expr.Expr.Expr() | 0 | 1 | 1 | 1 |
expr.Expr.addTerm(Term) | 0 | 1 | 1 | 1 |
expr.Expr.setExp(String) | 0 | 1 | 1 | 1 |
expr.Expr.toPoly() | 3 | 2 | 3 | 3 |
expr.Mono.Mono(BigInteger, int) | 0 | 1 | 1 | 1 |
expr.Mono.getCoe() | 0 | 1 | 1 | 1 |
expr.Mono.getExp() | 0 | 1 | 1 | 1 |
expr.Mono.toString() | 0 | 1 | 1 | 1 |
expr.Num.Num(String) | 0 | 1 | 1 | 1 |
expr.Num.getNum() | 0 | 1 | 1 | 1 |
expr.Num.setExp(String) | 0 | 1 | 1 | 1 |
expr.Num.toPoly() | 0 | 1 | 1 | 1 |
expr.Poly.Poly() | 0 | 1 | 1 | 1 |
expr.Poly.Poly(BigInteger, int) | 0 | 1 | 1 | 1 |
expr.Poly.addMono(Mono) | 0 | 1 | 1 | 1 |
expr.Poly.addPoly(Poly) | 5 | 1 | 4 | 4 |
expr.Poly.getMonoList() | 0 | 1 | 1 | 1 |
expr.Poly.getMonoMap() | 0 | 1 | 1 | 1 |
expr.Poly.monoToPoly(BigInteger, int) | 0 | 1 | 1 | 1 |
expr.Poly.mulPoly(Poly) | 3 | 1 | 3 | 3 |
expr.Poly.powPoly(int) | 1 | 1 | 2 | 2 |
expr.Poly.toString() | 2 | 2 | 2 | 3 |
expr.Power.Power(String, String) | 0 | 1 | 1 | 1 |
expr.Power.setExp(String) | 0 | 1 | 1 | 1 |
expr.Power.toPoly() | 0 | 1 | 1 | 1 |
expr.Term.Term(int) | 1 | 1 | 2 | 2 |
expr.Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
expr.Term.merge() | 3 | 1 | 3 | 3 |
expr.Term.negate() | 0 | 1 | 1 | 1 |
expr.Term.setExp(String) | 0 | 1 | 1 | 1 |
expr.Term.toPoly() | 1 | 1 | 2 | 2 |
各个方法的复杂度都还可以,在化简多项式那里略有复杂
第二次作业分析
UML
任务分析
- 处理新增的因子
exp()
- 处理自定义函数
- 处理多层括号的嵌套
- 去括号与化简
关于多层括号的处理,因为使用了递归下降算法,可以直接处理多层括号的问题
exp()
因子解析任务
分析
exp()
因子括号内是表达式因子,而引入exp()
因子后表达式因子的结构发生了变化,如下
M o n o = c o e ∗ x e x p o n e n t ∗ e x p ( ( P o l y ) ) Mono=coe * x ^{exponent}*exp((Poly)) Mono=coe∗xexponent∗exp((Poly))
P o l y = ∑ M o n o Poly = \sum Mono Poly=∑Mono
可见 Poly
的属性不需要改变,需要改变 Poly
中 Mono
的存储结构,原来的 HashMap
结构如果要适配新增的 exp()
因子会使结构变得极为复杂,所以我决定采用 ArrayList
存储结构
exp()
因子可能带有指数,形如exp()^num
,为了减小复杂度,我应该在解析时将指数去掉,统一化为exp(Poly)
的形式
解析——新增 ExpFactor
类
ExpFactor
// ExpFactor.java
......
private Factor factor; // exp() 括号内的内容
private String exp = "1"; // 指数函数整体的 指数
public Poly toPoly() {...} // 实现去指数
......
- 更新
Parser
识别到 exp()
因子后,先递归解析括号内的 Expr因子
,解析完内部因子再解析指数,将解析结果作为一个 ExpFactor
返回
// Parser.java
......
public Factor parseFactor() {
......
if (lexer.peek().getType() == Token.Type.EXPFUN) { // exp 函数因子
lexer.nextToken(); // 跳过 exp
lexer.nextToken(); // 跳过左括号
Factor inside = parseFactor(); // exp() 内部解析
lexer.nextToken(); // 跳过右括号
return new ExpFactor(inside, parseIndex());
}
......
}
自定义函数解析任务
分析
这个任务可以拆分成两部分,一个是函数定义式的处理,另一个是函数的调用。函数的定义在最开始出现,之后表达式解析时可能会有函数调用。所以新建 Definer
工具类,用来处理上述两部分。
Definer
// Definer.java
......
private static HashMap<String, String> funcMap = new HashMap<>(); // f/g/h -> 定义式
private static HashMap<String, ArrayList<String>> paraMap = new HashMap<>(); // f/g/h->x/y/z
......
其中 funcMap
是函数名到函数体的映射, paraMap
是函数名到函数形参的映射
函数定义式的处理
函数定义式形如
f
/
g
/
h
(
x
,
y
,
z
)
=
E
x
p
r
f/g/h(x,y,z) = Expr
f/g/h(x,y,z)=Expr
形参数量不确定,有 1 ~ 3 个,形参只用 xyz
,所以用正则表达式来匹配对应的形参。
// Definer.java
......
public static void addFunc(String input) { // 新增一个自定义函数
String[] parts = input.trim().split("=");
// 处理前半部分
......
String funcName = ""; // 函数名
ArrayList<String> paraList = new ArrayList<>();
...... // 匹配形参并将形参映射为非 x/y/z
paraMap.put(funcName, paraList);
......
// 处理函数体
StringBuffer result = new StringBuffer();
...... // 匹配函数体里的 x/y/z 并根据之前确定的映射规则映射为非 x/y/z
funcMap.put(funcName, result.toString());
}
......
处理好的函数定义式存储在静态区,会始终存在直到程序停止运行。
函数调用的处理
函数调用任务可以理解为:解析表达式里的函数调用式(形如f(实参1...)
),实现实参替换形参,将替换后的得到的表达式因子返回。
为了统一形式,新建 FunFactor
类
// FunFactor.java
......
private String newFunc; // 将函数实参带入形参位置后的结果(字符串形式)
private Expr expr; // 将newFunc解析成表达式后的结果
......
- 实参替换形参
Definer
里新增 callFunc()
方法,该方法接收两个参数(函数名和实参列表),根据已有的 funcMap 和 paraMap
实现实参替换形参
// Definer.java
......
public static String callFunc(String name, ArrayList<Factor> actualParas) { // 形参替换实参
String funcExpr = funcMap.get(name); // 函数表达式
ArrayList<String> paraList = paraMap.get(name); // 名为 name 函数的参数列表
for (int i = 0; i < actualParas.size(); ++i) {
funcExpr = replacePara(funcExpr, paraList.get(i), actualParas.get(i).toString());
}
return funcExpr;
}
public static String replacePara(String strReplaced, String para, String replacementExpr) {
// strReplaced -> 要被替换的字符串 | para -> 要被替换的内容 | replacementExpr -> 要替换为什么
StringBuffer result = new StringBuffer();
...... // 实现替换
return result.toString();
}
......
-
解析自定义函数因子
-
在
parseFactor()
里新增处理自定义函数的逻辑// Parser.java ...... public Factor parseFactor() { ...... if (lexer.peek().getType() == Token.Type.FUN) { // 自定义函数因子 解析 String funcName = lexer.peek().getContent(); lexer.nextToken(); // 跳过函数名 lexer.nextToken(); // 跳过 ( Factor funcFactor = parseFuncFactor(funcName); lexer.nextToken(); // 跳过 ) return funcFactor; } ...... } public Factor parseFuncFactor(String funcName) { ArrayList<Factor> actualParas = new ArrayList<>(); // 实参集合 actualParas.add(parseFactor()); while (lexer.peek().getType() != Token.Type.RP) { // 不是 ),也就不是结尾 lexer.nextToken(); // 跳过 , actualParas.add(parseFactor()); // 增加一个实参 } return new FuncFactor(funcName,actualParas); } ......
-
在
FuncFactor
的构造函数里实现具体的解析// FuncFactor.java ...... public FuncFactor(String name, ArrayList<Factor> actualParas) { this.newFunc = Definer.callFunc(name, actualParas); this.expr = setExpr(); // 将其作为 Expr 因子进行解析 } public Expr setExpr() { String str = Adjust.simplifyString(newFunc); Lexer lexer = new Lexer(str); Parser parser = new Parser(lexer); return parser.parseExpr(); } ......
-
化简任务
总体的化简架构与上次作业类似,将所有单位都视为 Poly
,利用 addPoly() 和 mulPoly()
方法实现去括号和合并同类相。
新增 exp()
因子后,对于 Poly.mulPoly() 和 Mono.mul()
两个方法影响不大,但是增加了加法的复杂度,需要先判断两个 Mono
能否相加,所以在 Mono
类新增了 canAdd()
方法来判断两个Mono能否相加,重写了 Mono
和 Poly
的 equals()
方法(具体的逻辑见方法复杂度分析)。
-
Mono
// Mono.java ...... private BigInteger coe; // 底数 private BigInteger exp; // 指数 private Poly expExp; // exp 括号内的因子 public boolean canAdd(Mono mono) { // 两个 Mono 能否相加 if (Objects.equals(this.exp, mono.getExp())) { // x^n 部分指数相同 if (expExp.isPolyNull() && mono.getExpExp().isPolyNull()) { // exp() 为空 return true; } return expExp.equals(mono.getExpExp()); // 判断 exp() 的内容是否<相等> } return false; } @Override public boolean equals(Object object) { if (object instanceof Mono) { Mono mono = (Mono) object; if (Objects.equals(this.coe, mono.getCoe()) && Objects.equals(this.exp, mono.getExp())) { // x^n 指数相同 if (expExp.isPolyNull()) { // exp() 为空 return true; } return expExp.equals(mono.getExpExp()); // exp() 的内容是否相等 } return false; } return false; } ......
-
Poly
// Poly.java ...... @Override public boolean equals(Object object) { if (object instanceof Poly) { Poly poly = (Poly) object; if (monoList.isEmpty() && poly.getMonoList().isEmpty()) { return true; } else if ((!monoList.isEmpty()) && (!poly.getMonoList().isEmpty()) && monoList.size() == poly.getMonoList().size()) { return monoList.containsAll(poly.getMonoList()) && poly.getMonoList().containsAll(monoList); } return false; } return false; } ......
注意:
containsAll()
方法调用contains()
方法,而contains()
方法用equals()
方法实现,所以能实现我们想要的功能。在这里必须用两个containsAll()
方法,不然会有 bug
优化任务
我并没有选择实现 exp()
的化简逻辑,化简主要是通过求括号内全部因子或部分因子的最大公因数,将其提取到括号外。化简太为巧妙和复杂,直接放弃
Bug 修复
- 修复了指数不支持
BigInteger
类型的 bug - 修复了
expFactor
使用toString()
方法导致出现不合法表达式(exp(expr)
(本应是exp((expr))
))的 bug
方法复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
IoProcess.IoProcess(Scanner) | 0 | 1 | 1 | 1 |
IoProcess.processFunction() | 1 | 1 | 2 | 2 |
Main.main(String[]) | 0 | 1 | 1 | 1 |
expr.Adjust.parseCoefficient(String) | 1 | 2 | 1 | 2 |
expr.Adjust.simplifyPoly(String) | 10 | 3 | 6 | 7 |
expr.Adjust.simplifyString(String) | 0 | 1 | 1 | 1 |
expr.Adjust.simplifyTerm(BigInteger, int) | 9 | 8 | 6 | 8 |
expr.Definer.addFunc(String) | 14 | 1 | 9 | 10 |
expr.Definer.callFunc(String, ArrayList<Factor>) | 1 | 1 | 2 | 2 |
expr.Definer.replacePara(String, String, String) | 1 | 1 | 2 | 2 |
expr.ExpFactor.ExpFactor(Factor, String) | 0 | 1 | 1 | 1 |
expr.ExpFactor.setExp(String) | 0 | 1 | 1 | 1 |
expr.ExpFactor.toPoly() | 2 | 2 | 2 | 2 |
expr.ExpFactor.toString() | 3 | 3 | 4 | 4 |
expr.Expr.Expr() | 0 | 1 | 1 | 1 |
expr.Expr.addTerm(Term) | 0 | 1 | 1 | 1 |
expr.Expr.setExp(String) | 0 | 1 | 1 | 1 |
expr.Expr.toPoly() | 3 | 2 | 3 | 3 |
expr.Expr.toString() | 7 | 3 | 5 | 5 |
expr.FuncFactor.FuncFactor(String, ArrayList<Factor>) | 0 | 1 | 1 | 1 |
expr.FuncFactor.setExp(String) | 0 | 1 | 1 | 1 |
expr.FuncFactor.setExpr() | 0 | 1 | 1 | 1 |
expr.FuncFactor.toPoly() | 0 | 1 | 1 | 1 |
expr.FuncFactor.toString() | 0 | 1 | 1 | 1 |
expr.Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
expr.Lexer.getNumber() | 2 | 1 | 3 | 3 |
expr.Lexer.hasNext() | 0 | 1 | 1 | 1 |
expr.Lexer.nextToken() | 7 | 2 | 4 | 11 |
expr.Lexer.peek() | 0 | 1 | 1 | 1 |
expr.Mono.Mono(BigInteger, BigInteger) | 0 | 1 | 1 | 1 |
expr.Mono.Mono(BigInteger, BigInteger, Poly) | 0 | 1 | 1 | 1 |
expr.Mono.Mono(Poly) | 0 | 1 | 1 | 1 |
expr.Mono.add(Mono) | 0 | 1 | 1 | 1 |
expr.Mono.canAdd(Mono) | 4 | 3 | 3 | 4 |
expr.Mono.deepCop(Mono) | 0 | 1 | 1 | 1 |
expr.Mono.equals(Object) | 7 | 4 | 4 | 5 |
expr.Mono.getCoe() | 0 | 1 | 1 | 1 |
expr.Mono.getExp() | 0 | 1 | 1 | 1 |
expr.Mono.getExpExp() | 0 | 1 | 1 | 1 |
expr.Mono.mul(Mono) | 0 | 1 | 1 | 1 |
expr.Mono.simplifyFirstHalf() | 7 | 7 | 5 | 7 |
expr.Mono.toString() | 12 | 7 | 9 | 9 |
expr.Num.Num(String) | 0 | 1 | 1 | 1 |
expr.Num.getNum() | 0 | 1 | 1 | 1 |
expr.Num.setExp(String) | 0 | 1 | 1 | 1 |
expr.Num.toPoly() | 0 | 1 | 1 | 1 |
expr.Num.toString() | 0 | 1 | 1 | 1 |
expr.Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
expr.Parser.parseExpr() | 10 | 1 | 9 | 9 |
expr.Parser.parseFactor() | 10 | 6 | 7 | 7 |
expr.Parser.parseFuncFactor(String) | 1 | 1 | 2 | 2 |
expr.Parser.parseIndex() | 5 | 2 | 4 | 4 |
expr.Parser.parseTerm(int) | 6 | 1 | 7 | 7 |
expr.Poly.Poly() | 0 | 1 | 1 | 1 |
expr.Poly.addMono(Mono) | 0 | 1 | 1 | 1 |
expr.Poly.addPoly(Poly) | 11 | 5 | 6 | 7 |
expr.Poly.deepCop(Poly) | 0 | 1 | 1 | 1 |
expr.Poly.equals(Object) | 7 | 4 | 8 | 8 |
expr.Poly.getMonoList() | 0 | 1 | 1 | 1 |
expr.Poly.isPolyNull() | 0 | 1 | 1 | 1 |
expr.Poly.monoToPoly(BigInteger, BigInteger) | 0 | 1 | 1 | 1 |
expr.Poly.monoToPoly(Mono) | 0 | 1 | 1 | 1 |
expr.Poly.mulPoly(Poly) | 3 | 1 | 3 | 3 |
expr.Poly.powPoly(int) | 1 | 1 | 2 | 2 |
expr.Poly.toString() | 6 | 3 | 5 | 6 |
expr.Power.Power(String, String) | 0 | 1 | 1 | 1 |
expr.Power.setExp(String) | 0 | 1 | 1 | 1 |
expr.Power.toPoly() | 0 | 1 | 1 | 1 |
expr.Power.toString() | 2 | 3 | 2 | 3 |
expr.Term.Term(int) | 1 | 1 | 2 | 2 |
expr.Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
expr.Term.merge() | 3 | 1 | 3 | 3 |
expr.Term.negate() | 0 | 1 | 1 | 1 |
expr.Term.setExp(String) | 0 | 1 | 1 | 1 |
expr.Term.toPoly() | 1 | 1 | 2 | 2 |
expr.Term.toString() | 19 | 1 | 10 | 10 |
expr.Token.Token(Type, String) | 0 | 1 | 1 | 1 |
expr.Token.getContent() | 0 | 1 | 1 | 1 |
expr.Token.getType() | 0 | 1 | 1 | 1 |
addFunc()
方法复杂度超标,因为在读入函数时将x/y/z
全部换为了a/b/c
,使用了复杂的匹配和更换机制nextToken()
方法复杂度超标,因为新增了众多符号,switch-case
结构的分支众多,解决方式是将同类的合并在一起,再单独实现同类语法单位内部的语法分析,比如将+/-/*/(/)
都识别为“算符”Mono.equals() 和 Poly.equals()
方法复杂度超标,这是因为新增了exp()
因子,使判断相等的过程变得极为复杂:
两个 M o n o 是否相等 ⟹ 指数是否相等并且 e x p ( P o l y ) 的 P o l y 是否相等 ⟹ 两个Mono 是否相等\Longrightarrow指数是否相等并且exp(Poly) 的Poly是否相等\Longrightarrow 两个Mono是否相等⟹指数是否相等并且exp(Poly)的Poly是否相等⟹
对于 P o l y 1 中的每个 M o n o ,都能在 P o l y 2 中找到与之相等的 M o n o ,反之也成立 ⟹ 两个 M o n o 是否相等 对于Poly1中的每个Mono,都能在Poly2中找到与之相等的Mono,反之也成立\Longrightarrow两个Mono是否相等 对于Poly1中的每个Mono,都能在Poly2中找到与之相等的Mono,反之也成立⟹两个Mono是否相等
这是一个不断递归的过程,下降的终点是 exp()
因子内的表达式为空,也就是没有 exp
项存在
Mono.simplifyFirstHalf() 和 Mono.toString() 和Term.toString()
方法复杂度超标,这是因为加入了新的因子,使化简变得更加复杂、判断条件更为繁琐Poly.addPoly()
复杂度极高,有以下原因:- 由于新增
exp()
因子,所以舍弃了上次作业的HashMap
存储结构,改为了ArrayList
存储,加法运算时遍历更为复杂 - 为了不改变原数据,采取了深克隆的办法,在遍历过程中对深克隆的对象有删除操作。在判断并合并完所有能够相加的
Mono
后,向新的Poly
里加入未合并的Mono
。这使得addPoly()
方法中有一个二重循环、两个单层循环,结构较为复杂
- 由于新增
第三次作业分析
UML
任务分析
- 函数表达式可以调用已经定义的其它函数
- 新增求导算子
函数定义调用已定义的函数
要完成这个任务,只需要在解析函数体时将函数体视为 Expr
, 调用 parseExpr
方法。但这样做有一个前提,就是架构能够处理多变量。我只在解析部分实现了多变量,而没有在化简部分(Poly 和 Mono
)实现多变量,有三个原因:
- 一是任务在化简时只有单变量
x
,如果已经完成了函数调用,那么现有架构可以很好处理去括号和合并同类项; - 二是解析后的函数体,即使有括号也不会影响正确性(但会增加处理时间),因为解析与化简是分离的、不耦合的;
- 三是在化简部分实现多变量太复杂,需要修改原有存储结构,重写之前的
equals() 和 canAdd()
方法;而在解析部分实现多变量就很简单,只需要增加属性Var
就可以解决。
如果任务对时间复杂度有较高要求,那么必须实现多变量的化简,否则函数体的括号层数会相当惊人,造成极大的递归开销。
实现求导因子
由于求导可以嵌套,所以我用递归的方式解决,如下:
识别到
d
x
⟹
解析括号内的因子,得到
E
x
p
r
⟹
对
E
x
p
r
求导
识别到dx\Longrightarrow解析括号内的因子,得到Expr\Longrightarrow对Expr求导
识别到dx⟹解析括号内的因子,得到Expr⟹对Expr求导
⟹ 对构成 E x p r 的每一个 T e r m 求导后相加 ⟹ 对 T e r m 应用乘法法则求导 \Longrightarrow对构成Expr的每一个Term求导后相加\Longrightarrow对Term应用乘法法则求导 ⟹对构成Expr的每一个Term求导后相加⟹对Term应用乘法法则求导
⟹ 需要对构成 T e r m 的 F a c t o r 求导 \Longrightarrow需要对构成Term的Factor求导 ⟹需要对构成Term的Factor求导
-
对
Num
求导,得到 0 -
对
Power
求导,应用幂函数求导法则// Power.java ...... @Override public Factor derive() throws IOException, ClassNotFoundException { Term term = new Term(1); if (exp.equals("1")) { return term.addFactor(new Num("1")); // 对 x 求导,得 1 } else if (exp.equals("0")) { return term.addFactor(new Num("0")); // 对 x^0 求导,得 0 } else { ...... // 幂函数求导法则 } } ......
-
对
ExpFactor
求导,应用指数函数求导法则// ExpFactor.java ...... @Override public Factor derive() throws IOException, ClassNotFoundException { if (exp.equals("0")) { return new Term(BigInteger.ZERO); } else { Term term = new Term(new BigInteger(exp)); // exp 为指数函数整体的幂次 Factor second = deepCop(this); // 深克隆 Factor first = factor.derive(); // 括号内求导 term.addFactor(first); term.addFactor(second); term.merge(); return term; } } ......
-
对
FuncFactor
求导,相当于对FuncFactor
内的expr
求导 -
对
Term
求导,应用乘法法则// Term.java ...... @Override public Factor derive() throws IOException, ClassNotFoundException { Expr expr = new Expr(); if (factors.isEmpty()) { Term t = new Term(BigInteger.ZERO); expr.addTerm(t); return expr; } for (int i = 0; i < factors.size(); i++) { Term t = new Term(new BigInteger(String.valueOf(coe))); t.addFactor(factors.get(i).derive()); for (int j = 0; j < factors.size(); j++) { if (i != j) { t.addFactor(factors.get(j)); } } t.merge(); expr.addTerm(t); } return expr; } ......
-
对
Expr
求导,直接对构成它的每个Term
求导再相加(如果有指数就使用幂函数求导法则)
方法复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
IoProcess.IoProcess(Scanner) | 0 | 1 | 1 | 1 |
IoProcess.processFunction() | 1 | 1 | 2 | 2 |
Main.main(String[]) | 0 | 1 | 1 | 1 |
expr.Adjust.simplifyString(String) | 0 | 1 | 1 | 1 |
expr.Definer.addFunc(String, boolean) | 10 | 1 | 7 | 7 |
expr.Definer.callFunc(String, ArrayList<Factor>) | 1 | 1 | 2 | 2 |
expr.Definer.convertPara(Pattern, String) | 5 | 1 | 4 | 5 |
expr.Definer.parseFuncBody(String) | 0 | 1 | 1 | 1 |
expr.Definer.replacePara(String, String, String) | 1 | 1 | 2 | 2 |
expr.ExpFactor.ExpFactor(Factor, String) | 0 | 1 | 1 | 1 |
expr.ExpFactor.deepCop(ExpFactor) | 0 | 1 | 1 | 1 |
expr.ExpFactor.derive() | 2 | 2 | 2 | 2 |
expr.ExpFactor.setExp(String) | 0 | 1 | 1 | 1 |
expr.ExpFactor.toPoly() | 2 | 2 | 2 | 2 |
expr.ExpFactor.toString() | 6 | 5 | 6 | 6 |
expr.Expr.Expr() | 0 | 1 | 1 | 1 |
expr.Expr.addTerm(Term) | 0 | 1 | 1 | 1 |
expr.Expr.deepCop(Expr) | 0 | 1 | 1 | 1 |
expr.Expr.derive() | 8 | 4 | 5 | 6 |
expr.Expr.isEmpty() | 0 | 1 | 1 | 1 |
expr.Expr.mergeExpr(Expr, Expr) | 2 | 3 | 1 | 3 |
expr.Expr.setExp(String) | 0 | 1 | 1 | 1 |
expr.Expr.toPoly() | 3 | 2 | 3 | 3 |
expr.Expr.toString() | 7 | 3 | 5 | 5 |
expr.FuncFactor.FuncFactor(String, ArrayList<Factor>) | 0 | 1 | 1 | 1 |
expr.FuncFactor.derive() | 0 | 1 | 1 | 1 |
expr.FuncFactor.setExp(String) | 0 | 1 | 1 | 1 |
expr.FuncFactor.setExpr() | 0 | 1 | 1 | 1 |
expr.FuncFactor.toPoly() | 0 | 1 | 1 | 1 |
expr.FuncFactor.toString() | 0 | 1 | 1 | 1 |
expr.Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
expr.Lexer.getNumber() | 2 | 1 | 3 | 3 |
expr.Lexer.hasNext() | 0 | 1 | 1 | 1 |
expr.Lexer.nextToken() | 8 | 2 | 5 | 12 |
expr.Lexer.peek() | 0 | 1 | 1 | 1 |
expr.Mono.Mono(BigInteger, BigInteger) | 0 | 1 | 1 | 1 |
expr.Mono.Mono(BigInteger, BigInteger, Poly) | 0 | 1 | 1 | 1 |
expr.Mono.Mono(Poly) | 0 | 1 | 1 | 1 |
expr.Mono.add(Mono) | 0 | 1 | 1 | 1 |
expr.Mono.canAdd(Mono) | 4 | 3 | 3 | 4 |
expr.Mono.deepCop(Mono) | 0 | 1 | 1 | 1 |
expr.Mono.equals(Object) | 7 | 4 | 4 | 5 |
expr.Mono.getCoe() | 0 | 1 | 1 | 1 |
expr.Mono.getExp() | 0 | 1 | 1 | 1 |
expr.Mono.getExpExp() | 0 | 1 | 1 | 1 |
expr.Mono.mul(Mono) | 0 | 1 | 1 | 1 |
expr.Mono.simplifyFirstHalf() | 7 | 7 | 5 | 7 |
expr.Mono.toString() | 12 | 7 | 9 | 9 |
expr.Num.Num(String) | 0 | 1 | 1 | 1 |
expr.Num.derive() | 0 | 1 | 1 | 1 |
expr.Num.getNum() | 0 | 1 | 1 | 1 |
expr.Num.setExp(String) | 0 | 1 | 1 | 1 |
expr.Num.toPoly() | 0 | 1 | 1 | 1 |
expr.Num.toString() | 0 | 1 | 1 | 1 |
expr.Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
expr.Parser.parseExpr() | 10 | 1 | 9 | 9 |
expr.Parser.parseFactor() | 11 | 7 | 8 | 8 |
expr.Parser.parseFuncFactor(String) | 3 | 1 | 3 | 3 |
expr.Parser.parseIndex() | 5 | 2 | 4 | 4 |
expr.Parser.parseTerm(int) | 6 | 1 | 7 | 7 |
expr.Poly.Poly() | 0 | 1 | 1 | 1 |
expr.Poly.addMono(Mono) | 0 | 1 | 1 | 1 |
expr.Poly.addPoly(Poly) | 11 | 5 | 6 | 7 |
expr.Poly.deepCop(Poly) | 0 | 1 | 1 | 1 |
expr.Poly.equals(Object) | 7 | 4 | 8 | 8 |
expr.Poly.getMonoList() | 0 | 1 | 1 | 1 |
expr.Poly.isPolyNull() | 0 | 1 | 1 | 1 |
expr.Poly.monoToPoly(BigInteger, BigInteger) | 0 | 1 | 1 | 1 |
expr.Poly.monoToPoly(Mono) | 0 | 1 | 1 | 1 |
expr.Poly.mulPoly(Poly) | 3 | 1 | 3 | 3 |
expr.Poly.powPoly(int) | 1 | 1 | 2 | 2 |
expr.Poly.toString() | 6 | 3 | 5 | 6 |
expr.Power.Power(String, String) | 0 | 1 | 1 | 1 |
expr.Power.derive() | 3 | 3 | 3 | 3 |
expr.Power.setExp(String) | 0 | 1 | 1 | 1 |
expr.Power.toPoly() | 0 | 1 | 1 | 1 |
expr.Power.toString() | 2 | 3 | 2 | 3 |
expr.Term.Term(BigInteger) | 0 | 1 | 1 | 1 |
expr.Term.Term(int) | 1 | 1 | 2 | 2 |
expr.Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
expr.Term.derive() | 7 | 2 | 5 | 5 |
expr.Term.isEmpty() | 0 | 1 | 1 | 1 |
expr.Term.merge() | 3 | 1 | 3 | 3 |
expr.Term.negate() | 0 | 1 | 1 | 1 |
expr.Term.setExp(String) | 0 | 1 | 1 | 1 |
expr.Term.toPoly() | 1 | 1 | 2 | 2 |
expr.Term.toString() | 19 | 1 | 10 | 10 |
expr.Token.Token(Type, String) | 0 | 1 | 1 | 1 |
expr.Token.getContent() | 0 | 1 | 1 | 1 |
expr.Token.getType() | 0 | 1 | 1 | 1 |
expr.Term.negate() | 0 | 1 | 1 | 1 |
expr.Term.setExp(String) | 0 | 1 | 1 | 1 |
expr.Term.toPoly() | 1 | 1 | 2 | 2 |
expr.Term.toString() | 19 | 1 | 10 | 10 |
expr.Token.Token(Type, String) | 0 | 1 | 1 | 1 |
expr.Token.getContent() | 0 | 1 | 1 | 1 |
expr.Token.getType() | 0 | 1 | 1 | 1 |
ExpFactor.toString()
方法复杂度较高,因为为了尽可能减少exp((factor))
的情况,加入了复杂的条件判断Expr.derive()
方法复杂度较高,因为通过递归的方式进行求导ExpFactor.toString()
方法复杂度较高,因为为了尽可能减少exp((factor))
的情况,加入了复杂的条件判断Expr.derive()
方法复杂度较高,因为通过递归的方式进行求导
关于 Bug
自己的bug
在第一次作业和第二次作业出现了 bug,都是在一些小细节的地方大意了,比如 toString()
方法不加括号、得到了字符串没有先判断是否是空串等。这些 bug 有一个共性,就是都出现在略复杂的方法中。这也提醒我要注意降低方法的圈复杂度,繁琐的逻辑中往往会潜藏危机。
关于互测
我主要采取两个办法:
- 根据自己编写过程中的 bug 构造相应数据
- 使用极端值
这两种方法在互测中用处不大,最管用的办法应该是阅读其它同学的源码,针对性构造样例(我只在第一次作业使用该方法,虽然好用但是耗时巨大)
心得体会
- “永远不要相信别人传给你的数据”,在出现数据交互时,一定要检查得到的数据是否合法、是否正确
- 一定要重视架构的可拓展性。第一次作业在
Mono
类里采用了HashMap
的存储方式,虽然简化了第一次作业的代码逻辑,但是难以拓展为其他因子,使得第二次作业出现了小范围重构。 - 为了防止难以预测的 bug,我并没有实现更进一步的优化算法。现在想来略感遗憾,这样做虽然保证了正确率,但也失去了一部分乐趣
- 要重视代码结构的设计,面向对象编程而非面向题目编程,让代码具有美
- 通过上学期的
OOpre
,这学期的 OO 正课上手较快、体验极好。很难想象如果没有提前学习Java
语言该如何应对复杂的任务
未来方向
可以在每次作业时增加对下次迭代内容的“暗示”,“暗示”一个大方向,强化大家对于“可拓展性”架构的设计