301
社区成员
发帖
与我相关
我的任务
分享第一单元的主题是支持单变元、指数函数、自定义函数与求导因子的嵌套表达式括号展开,主要的学习目标是建立对面向对象程序设计的认识,掌握其框架和输入处理、主控、核心数据管理三个关键类;在迭代的大背景下认识对象;掌握层次化设计,对多层次对象进行管理。本单元一共有三次作业,分别是单层括号多项式展开,含有指数函数、单层自定义函数调用的多项式展开,含有求导因子的多层嵌套自定义函数的表达式展开。单元的两次实验分别是递归下降法计算表达式的值,对 Expr - Term - Factor 结构的表达式进行求导。
可以看出,这三次作业的要求是一步步递进的,而两次实验对作业具有极大的指导作用。一定要重视实验!!!随着迭代要求的加深,代码行数膨胀,复杂度大大增加。不过好在经过一个月的挣扎、反思、code、debug 后,第一单元终于落下了帷幕,实现一份完整代码的巨大愉悦感是 overwhelming 的。以下是我对本单元作业的代码的详细分析与这个月的面向对象程序设计的学习心得。
通过对第一单元指导书的阅读与对题目的分析,我将表达式解析为三层——Expr,Term,Factor,而 Factor 又有三种——幂函数,常数因子和表达式因子。这里由于顶层的表达式是一个“入口”,并且可以认为其指数一直为1,对于形如 (x+y+2*z)^2 的表达式,可以认为这是一个仅有单个 Factor 组成的 Expr ,不妨再建立一个类 ExprFactor 单独存储表达式因子以区分顶层表达式。
考虑到 Term 和 Factor 有可能重复,集合类容器不能存储相同项(当然也可能可以用奇怪的方法),尽管项是无序的,很适合用 Hash 相关容器,但保险起见我还是用了列表来存储,Expr 类中使用 ArrayList 来容纳该表达式中含有的 Term ,Term 类中使用 ArrayList 来容纳该项中含有的 Factor。
初步分析过后,我们需要开始解决表达式的解析和计算问题。
输入处理
我通过字符串的替换方法,删去空格,将连续的加减号替换成唯一的一个。这里由于有可能有多于2个符号,故需要循环直到不再有为止。这部分方法主要是简单的字符串操作,和其他类的耦合很松,不适合放到对应的类中,故我新建了工具类 Tools。
public class Tools {
// ...
public static String mergeOperator(String s) {
String res = s;
while (true) {
if (res.contains("++")) {
res = res.replace("++", "+");
} else if (/*...*/) {
// ...
}
// ...
}
return res;
}
public static String preprocess(String input) {
String s = input;
s = s.replaceAll("[ \\t]", "");
s = mergeOperator(s);
return s;
}
public static String trim(String input) {
// 删去头尾的加减号
}
}
作业是的实现参考了我去年的架构,值得一提的是,我在处理去年的 ** 符号的时便将 ** 替换成 ^,这个小trick反而帮了我不少忙,避免了对于两个 * 的讨论。启示我不需要太过关注文法的表面信息,而只需在意其分割的作用和分割的语义。
表达式解析:递归下降算法
对于第一次作业,可以采用正则表达式匹配的方法。但是正则表达式的判断书写庞杂,在 OO Pre 先导课的最后一单元我采取的正则表达式方法,最后因为一个括号没加转义符白白浪费半天 debug 时间。这里也是一点小遗憾吧,上学期最后一次作业时间仓促没有来得及学会递归下降算法,立志这学期一定要用递归下降。所以秉持着对自己的挑战和对迭代结构的负责,我没有丝毫犹豫,直接选择了递归下降方法。
递归下降算法,经过我三次作业的理解与实战后,发现算法本身并不难,但是涉及到三层层次结构的相互递归调用,非常抽象。这里最重要的一个点是,递归是写给机器看的,人不需要对每一个细枝末节都了解得那么透彻(当然越透彻越好,但是我能力有限嘛)。递归下降的两个部分,词法分析和语法分析,分别由 Lexer(词法分析器)和 Paser(语法解析器)完成。Lexer 将表达式分解成基本语法单元 token,如运算符、数字等,Parser 根据 Lexer 解析出的 token,递归生成表达式、项和因子。这两个类的实现我参考了训练中的框架。
Lexer
这里采取我们在数据结构课程中用到的类似方法,遍历字符串,分析 token,将结果返回 curToken 这个字符串:
public class Lexer {
private final String input; // 存储输入的赋值语句
private int pos = 0; // 用于记录当前读取到的位置
private String curToken;
public Lexer(String input) {/*...*/}
public void next() {
// ...
if (Character.isDigit(c)) {
curToken = getNumber();
} else if ("xyz+-*()^".indexOf(c) != -1) {
pos += 1;
curToken = String.valueOf(c);
} else {
System.out.println("Wrong Input");
}
}
public String peek() {
return this.curToken;
}
}
Parser
不同于练习题的项与项仅有 + 号,这里的实现需要考虑符号,但是仅表达式的第一项和项有符号,将符号归入项后,可以认为仅有项有符号。
所以整合后的结构如下:
public Expr parseExpr() {/*...*/}
public Term parseTerm() {
Term term = new Term();
int sign;
// getSign
term.setSign(sign);
term.addFactor(parseFactor());
while (lexer.peek().equals("*")) {/*...*/}
return term;
}
public Factor parseFactor() {
if (lexer.peek().equals("(")) {
// parseExpr
return new ExprFactor(/*...*/);
} else if ("xyz".contains(lexer.peek())) {
// parsePower
return new Power(/*...*/);
} else { // BigInteger part
// parseNumber
return new Number(num);
}
}
表达式展开:转化为 Poly-Mono 结构
观察到本次作业中表达式展开的最终结果是由若干个如下的单项式组成的多项式的形式
$$
Poly=\sum Mono
$$
$$
Mono = coeff \times x^{exponent}
$$
故我设置 Poly 多项式类和 Mono 单项式类用于计算。
接下来面临的问题是将 Expr - Term - Factor 三层的存储结构转化为 Poly - Mono 结构,于是考虑自顶向下展开,Expr 由 Term 相加得到,故只需要将每个 Term 转化为 Poly,再求其和即可,而对于 Term 结构,只需将其 Factor 先转化成 Poly 再相乘即可。
Number (数字因子)和 Power(幂函数因子)的 toPoly 方法: 转化为仅含此Mono的Poly。如 42可以转化为仅含42*x^0 单项式的多项式,x^5 转化为仅含 1*x^5 单项式的多项式。
考虑到三个实现了 Factor 接口的类都有 toPoly() 方法,根据接口的行为抽象特点,在 Factor 中添加 toPoly() 抽象方法,在每个实现接口的类中都重写该方法。并需要在 Poly 类中创建 addPoly() 、 mulPoly() 和 powPoly() 方法用于计算,simpPoly() 方法用于化简多项式。
两个类的大致内容如下:
public class Mono {
private BigInteger coeff;
private HashMap<String, Long> varmap = new HashMap<>();
// ...
public Mono mulMono(Mono other) { /*...*/ } // 新建方法,返回两个Mono相乘结果
public boolean isZero() { /* 判断是否为0,用于遍历删除 */ }
public boolean isNum() { /* 判断是否为纯数字,用于输出优化 */ }
public boolean isSimilar(Mono other) {
return varMap.equals(other.getVarMap()); // 用HashMap的equals方法判断是否可以合并
}
}
public class Poly {
private ArrayList<Mono> monoList = new ArrayList<>();
public void addMono(Mono mono) { /*...*/ }
public Poly addPoly(Poly other) { /*...*/ }
public Poly mulPoly(Poly other) { /*...*/ }
public Poly powPoly(long times) {
// 这个方法从0开始遍历,天然可以处理表达式的零次幂
Mono mono = new Mono(BigInteger.ONE);
Poly p = new Poly();
p.addMono(mono); // Poly's value is 1
for (long i = 0; i < times; i++) {
p = p.mulPoly(this);
}
return p;
}
public Poly simpPoly() {
// 两层for循环合并同类项,需要重写equals和hashcode方法,在最终需要用removeIf函数真正删去为0的Mono
monoList.removeIf(Mono::isZero); // 真删除才会提高后续toString的运行速度
}
}
系数为0,则最终结果为0
系数为1 或 -1,则可以省略系数
变量指数为0,则可以省略该变量
变量指数为1,则可以省略指数
首个 Mono 符号为 - 可以将正项提前
第二次作业添加了嵌套括号、指数函数和自定义函数。嵌套括号由于第一次作业采取了递归下降算法解析字符串,不需要特意修改,沿用即可。指数函数因子的内层我也采取 Poly 形式存储,考虑到会出现指数,还需要在类中设置指数成员。而由于自定义函数中 xyz 出现的顺序不一定,如 f(y,z,x) = 11*x + 14*y + 15*z,所以我采取的方式是,只要识别到 xyz 就按出现的先后顺序,替换定义式中相应的字母。考虑到有可能有表达式因子为形参的情况,需要在替换时在被替换的字符两端加入 () 保证其正确性,套用上面的例子就是改写为 f(u,v,w) = 11*(v) + 14*(u) + 15*(w)。
用字符串处理简单方便,当然这也带来了复杂度的提高。
Mono 的结构发生了如下的变化:
$$
Mono = coeff \times x^{exponent} \times exp(poly_1) \times exp(poly_2) \times \dots \times exp(poly_n)
$$
自定义函数替换
// Main.java
if (Config.hasCustomFunction) {
int functionCount = Integer.parseInt(scanner.nextLine());
for (int i = 0; i < functionCount; i++) {
// 预处理后解析为字符串传入函数表
Lexer functionLexer = new Lexer(functionBody);
Parser functionParser = new Parser(functionLexer);
functionBody = functionParser.parseExpr().toPoly().toString();
String function = strings[0] + "=" + functionBody;
tool.addFunction(function);
}
}
String s = scanner.nextLine();
String input = tool.preprocess(s);
// 可能会出现多层嵌套调用的情况,所以要while
while (input.contains("f") | input.contains("g") | input.contains("h")) {
input = tool.inputParas(input); // 逐一替换实参直至无自定义函数
}
值得一提的是,函数从左到右的解析先后顺序和函数嵌套调用的前后顺序是一致的,如
f(g(x)),先解析的是f,再解析g。当然这里面的先后调用似乎也没什么关系,所以还是要把字符串替换写得更严谨些。
public String inputParas(String in) {
// String input = in.replaceAll("[ \\t]", "");
String input = in;
int[] index = new int[3];
index[0] = input.indexOf("f");
index[1] = input.indexOf("g");
index[2] = input.indexOf("h");
int min = -1;
String curName = "";
for (int i = 0; i < 3; i++) {
if (index[i] > -1 && index[i] > min) {
min = index[i]; // update min
curName = String.valueOf("fgh".charAt(i));
}
}
int curBra = 0; // 栈变量
String s = input.substring(min);
// 如x+f(x,y)+g(y,z),此时s = f(x,y)
for (int i = 0; i < s.length(); i++) {
if ("(".indexOf(s.charAt(i)) != -1) { // left
curBra++;
} else if (")".indexOf(s.charAt(i)) != -1) { // right
curBra--;
}
if (i != 0 && curBra == 0) {
s = s.substring(0, i + 1); // 提取整个f作用范围
break;
}
}
// 先截取函数因子的范围
String actualArgv;
int start = 2;
int argc = 0;
if (functionMap.get(curName).getDefString() != null) {
String actual = functionMap.get(curName).getDefString();
for (int i = 0; i < s.length(); i++) {
if ("(".indexOf(s.charAt(i)) != -1) {
curBra++;
} else if (")".indexOf(s.charAt(i)) != -1) {
curBra--;
} else if (",".indexOf(s.charAt(i)) != -1 && curBra == 1) {
// 匹配到逗号且当前curBra=1,证明是一个分割参数的位置
actualArgv = s.substring(start, i);
actual = actual.replace(String.valueOf("uvw".charAt(argc)), actualArgv);
argc++;
start = i + 1; // 记得+1
}
if (i != 0 && curBra == 0) {
// 最后一个参数
actualArgv = s.substring(start, s.length() - 1);
actual = actual.replace(String.valueOf("uvw".charAt(argc)), actualArgv);
argc++;
}
}
input = input.replace(s, actual);
}
input = mergeOperator(input);
return input;
}
由于用一个函数实现了若干函数的判断,这个函数较长且有些耦合。
具体的思路是存储函数定义式后,按出现先后顺序处理 fgh(由于不保证顺序出现),再依靠括号栈分析出其作用范围,最后用逗号和栈的深度匹配对应实参替换。
虽然字符串处理不是特别面向对象的方法,但是用栈处理作用范围和实参范围还是有些巧妙的~
指数处理
指数函数不能加入原有的类,需要新建一个实现 Factor 接口的类 Exp,并在 Mono 中加入类似 varMap 的 expMap,键存储 exp 内部的 Poly,值用来存储这个项的指数。
由于要实现后续的优化,故我在由
Exp构造Mono时将所有的指数降幂,将指数乘入底数。
新建 Exp 类:
public class Exp implements Factor {
private final Poly base;
private final long exponent;
@Override
public Poly toPoly() {
Poly poly = new Poly();
Mono mono = new Mono(base, exponent);
poly.addMono(mono);
return poly;
}
}
修改后的 Mono 如下:
public class Mono {
public Mono(BigInteger coe) { /* 常数构造 */ }
public Mono(String base, long exponent) { /* 幂函数构造 */ }
public Mono(Poly base, long exponent) {
// 指数函数构造
this.coe = BigInteger.ONE;
// initialize hashmaps
Poly poly = base.simpPoly();
Poly exponentPoly = new Number(BigInteger.valueOf(exponent)).toPoly();
poly = poly.mulPoly(exponentPoly);
if (!poly.isDefiniteZero()) {
// 优化:此处如果Poly已经确定为0了,整个exp项为1,不需要添加入expMap中
expMap.put(poly, 1L);
}
}
// ...
}
指数因子和自定义函数的引入产生了新的文法,需要更改 Lexer 和 Parser 适应这种改变。
具体过程为, next 方法添加如下内容:
public void next() {
// ...
if ("e".indexOf(c) != -1) {
pos += 3;
curToken = "exp"; // 解析到e吃入exp整个符号,并将光标后跳3
} else {
// ...
} // 函数的解析我采取了预处理替换字符串的方法,故不会影响Lexer
}
只有 parseFactor 需要调整:
public Factor parseFactor() {
// ...
if (lexer.peek().equals("exp")) {
parseExp();
} else {
// ...
}
}
本次的作业增加了求导因子,添加了“新定义的函数可以调用已定义的函数”的规则,并且求导因子可以出现在函数定义式中。
求导因子的实现思路较为清晰,对于 Poly - Mono 两层结构,Poly 的求导结果为各 Mono 求导结果之和,故关键问题在于 Mono 的 derive() 方法,仅需分两个部分(幂函数部分和指数函数部分),再用链式法则即可。
对于函数定义式的条件变化,我的想法是,如果分开讨论求导因子出现的位置,代码冗余,而且就函数解析的实现来看,也没有必要。题目中的提示信息为,可以调用已定义的函数,并且可以出现求导因子。故只需要将函数的定义式(也是表达式) 同待求表达式一样解析即可。我的处理方式是用 = 分割字符串取得定义式,此时便将形参替换为实参,后续照常处理即可。
求导因子
新建 Derivative 类,内部存储需要求导的 Poly 即可。
// Parser.java
public Factor parseFactor() {
// ...
if (lexer.peek().equals("d")) {
parseDerivative();
} else {
// ...
}
}
这个类仅仅做暂存,为保证
Parser和因子toPoly形式上的一致性。
Mono 的求导方法
// 注意到 Mono 的求导返回结果就是 Poly
public Poly derive() {
Poly poly = new Poly();
// 只对多项式求导的Mono
Mono pre = this.clone();
long exponent = pre.getVarMap().get("x");
if (exponent != 0L) {
// 按道理都不是常数,但是稳妥起见
// ...
poly.addMono(pre);
}
// 对每个exp求导
for (Map.Entry<Poly, Long> entry : expMap.entrySet()) {
// ...
}
return poly;
}
函数定义式的解析
在传入函数表时候便解析定义式。
while (functionBody.contains("f") || functionBody.contains("g")
|| functionBody.contains("h")) {
functionBody = tool.inputParas(functionBody);
}
第三次代码的迭代开发量是最小的,数个小时内就能完成,堪称业界良心。
上面当然是开玩笑,其实这体现了可复用性的思想和优良的架构带给软件开发的巨大便利。
| method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Mono.toString() | 31.0 | 7.0 | 14.0 | 15.0 |
| Tools.inputParas(String) | 24.0 | 3.0 | 13.0 | 17.0 |
| Poly.simpPoly() | 10.0 | 1.0 | 7.0 | 7.0 |
| Parser.parseFactor() | 9.0 | 5.0 | 7.0 | 7.0 |
| Poly.equals(Object) | 9.0 | 8.0 | 3.0 | 9.0 |
| Lexer.next() | 8.0 | 2.0 | 7.0 | 8.0 |
| Main.main(String[]) | 8.0 | 1.0 | 7.0 | 7.0 |
| Tools.mergeOperator(String) | 7.0 | 6.0 | 6.0 | 6.0 |
| Mono.isSingleNum() | 6.0 | 5.0 | 1.0 | 5.0 |
| Tools.trim(String) | 6.0 | 5.0 | 4.0 | 5.0 |
| Mono.mulMono(Mono) | 5.0 | 1.0 | 4.0 | 4.0 |
| Poly.isSingle() | 5.0 | 3.0 | 2.0 | 4.0 |
| Mono.Mono(String, long) | 4.0 | 1.0 | 3.0 | 3.0 |
| Mono.derive() | 4.0 | 1.0 | 4.0 | 4.0 |
| Mono.equals(Object) | 4.0 | 4.0 | 3.0 | 6.0 |
| Mono.isSinglePower() | 4.0 | 4.0 | 1.0 | 4.0 |
| Parser.parseTerm() | 4.0 | 1.0 | 4.0 | 4.0 |
| Mono.simplify() | 3.0 | 1.0 | 3.0 | 3.0 |
| Parser.parseExponent() | 3.0 | 1.0 | 3.0 | 3.0 |
| Poly.mulPoly(Poly) | 3.0 | 1.0 | 3.0 | 3.0 |
| Poly.toString() | 3.0 | 2.0 | 3.0 | 3.0 |
| Term.toString() | 3.0 | 1.0 | 3.0 | 3.0 |
| Lexer.getNumber() | 2.0 | 1.0 | 3.0 | 3.0 |
| Parser.parseExpr() | 2.0 | 1.0 | 3.0 | 3.0 |
| Poly.isDefiniteZero() | 2.0 | 1.0 | 4.0 | 4.0 |
| Term.toPoly() | 2.0 | 1.0 | 3.0 | 3.0 |
| Expr.toPoly() | 1.0 | 1.0 | 2.0 | 2.0 |
| ExprFactor.toPoly() | 1.0 | 1.0 | 2.0 | 2.0 |
| Mono.Mono(Poly, long) | 1.0 | 1.0 | 2.0 | 2.0 |
| Mono.isSimilar(Mono) | 1.0 | 1.0 | 2.0 | 2.0 |
| Mono.isSingle() | 1.0 | 1.0 | 2.0 | 2.0 |
| Poly.derive() | 1.0 | 1.0 | 2.0 | 2.0 |
| Poly.negate() | 1.0 | 1.0 | 2.0 | 2.0 |
| Poly.powPoly(long) | 1.0 | 1.0 | 2.0 | 2.0 |
| Power.toString() | 1.0 | 1.0 | 2.0 | 2.0 |
| Tools.addFunction(String) | 1.0 | 1.0 | 2.0 | 2.0 |
| Derivative.Derivative(Poly) | 0.0 | 1.0 | 1.0 | 1.0 |
| Derivative.toPoly() | 0.0 | 1.0 | 1.0 | 1.0 |
| Exp.Exp(Poly, long) | 0.0 | 1.0 | 1.0 | 1.0 |
| Exp.getBase() | 0.0 | 1.0 | 1.0 | 1.0 |
| Exp.toPoly() | 0.0 | 1.0 | 1.0 | 1.0 |
| Exp.toString() | 0.0 | 1.0 | 1.0 | 1.0 |
| Expr.Expr() | 0.0 | 1.0 | 1.0 | 1.0 |
| Expr.addTerm(Term) | 0.0 | 1.0 | 1.0 | 1.0 |
| ExprFactor.ExprFactor(Expr, long) | 0.0 | 1.0 | 1.0 | 1.0 |
| Function.Function(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| Function.getDefString() | 0.0 | 1.0 | 1.0 | 1.0 |
| Lexer.Lexer(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| Lexer.peek() | 0.0 | 1.0 | 1.0 | 1.0 |
| Lexer.toString() | 0.0 | 1.0 | 1.0 | 1.0 |
| Mono.Mono(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
| Mono.Mono(BigInteger, HashMap, HashMap) | 0.0 | 1.0 | 1.0 | 1.0 |
| Mono.clone() | 0.0 | 1.0 | 1.0 | 1.0 |
| Mono.getCoe() | 0.0 | 1.0 | 1.0 | 1.0 |
| Mono.getExpMap() | 0.0 | 1.0 | 1.0 | 1.0 |
| Mono.getVarMap() | 0.0 | 1.0 | 1.0 | 1.0 |
| Mono.hashCode() | 0.0 | 1.0 | 1.0 | 1.0 |
| Mono.isZero() | 0.0 | 1.0 | 1.0 | 1.0 |
| Mono.setCoe(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
| Number.Number(BigInteger) | 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.Parser(Lexer) | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.Poly() | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.addMono(Mono) | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.addMonos(List) | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.addPoly(Poly) | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.getMonoList() | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.hashCode() | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.size() | 0.0 | 1.0 | 1.0 | 1.0 |
| Power.Power(String, long) | 0.0 | 1.0 | 1.0 | 1.0 |
| Power.getExponent() | 0.0 | 1.0 | 1.0 | 1.0 |
| Power.toPoly() | 0.0 | 1.0 | 1.0 | 1.0 |
| Term.Term() | 0.0 | 1.0 | 1.0 | 1.0 |
| Term.addFactor(Factor) | 0.0 | 1.0 | 1.0 | 1.0 |
| Term.setSign(int) | 0.0 | 1.0 | 1.0 | 1.0 |
| Tools.Tools() | 0.0 | 1.0 | 1.0 | 1.0 |
| Tools.preprocess(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| Total | 181.0 | 121.0 | 181.0 | 206.0 |
| Average | 2.2911392405063293 | 1.5316455696202531 | 2.2911392405063293 | 2.607594936708861 |
从代码复杂度分析数据可以看到,大多数方法的复杂度在合理范围之内,而有五个方法复杂度超标。
Mono 类中 toString() 方法在生成单项式的字符串形式时,需要大量的特判语句,复杂度较难降低。
在 Parser 类的 parseFactor() 方法中,对当前解析的因子类型判断与分别解析导致了代码复杂度较高。 一种改进方法是单独封装每一种因子的解析方法,然后在 parseFactor() 中按因子类型调用这些方法。
Tools 类中的 inputParas() 和 trim() 方法对 StringBuffer 及字符串对象频繁进行操作与判断,结构复杂。 一种改进方法是采用对象引用替换的方法代替对字符串对象的操作
第三次作业代码的UML类图如下所示:

其中 Lexer 和 Parser 类用递归下降法解析输入字符串,存入 Expr - Term - Factor 这一三层存储结构。Tools 和 Funct 类用于对字符串进行预处理以及处理函数的定义及调用。Mono 和 Poly 类形成了两层计算结构用于计算表达式的值。在三层结构中的每层结构都有 toPoly() 方法将对应的结构转化为二层的计算结构。
本单元第一次作业在强测中测出了 ctle,原因是在表达式的幂次时,未先化简内部再继续运算,且未将为 0 的 Mono 真正删除导致反复判断。
改进方法是重写 hashcode() 和 equals() 方法,并真正删除为 0 的 Mono。简单注释掉优化的方法虽然能通过评测,但是对代码能力的提升于事无补,可以称作自我放纵。希望看到这篇拙劣文章的读者能有则改之无则加勉。
本次测试采取的是自动评测系统+手动构造。自动评测系统来自评论区的热心 dl,手动构造样例主要是构造边缘化样例,如高指数引起的大代价数据,多层指数函数嵌套数据,自定义函数名和参数表非字典序排列数据、函数调用实参为指数求导因子,求导内容为自定义函数等等。如:
0 (x^8+y^6+z^7)^7 1 f(x)=... dx(exp((exp((exp((exp((f(x)))))))))) 3 h(z,x)=... f(x,y)=... g(y,x,z)=... ... 1 f(x,z)=... f(dx(exp((x^3+y^2))))+... 1 f(x,z)=... dx(f(exp((x^3+y^2))), exp((y^3+x^2)))+...
本次测试中测试出自己的代码在几千组随机数据中有多组 tle,当时并未引起重视,直到强测给了我狠狠一巴掌(
在这三次作业中,我在写第一次作业的过程中进行了重构。本来打算采取 Expr - Term - Factor 的存储结构和计算结构,但是在实现过程中发现这种方式用于计算有些”烧脑“,递归分析不清。于是转而采取了这种存储结构和 Poly - Mono 的计算结构,通过将 Expr - Term - Factor 多层嵌套存储结构转化为 Poly - Mono 双层计算结构,降低了计算时的思维难度。而采取递归下降算法的预见性让我避免了从第一次到第二次作业的大重构。
整体上我对自己代码的架构设计相对满意,总体行数也在千行左右,实现了简单的优化,并无过分丑陋的特判,自认为结构清晰不冗余。
架构的不足之处在于未能用对象替换方法解决自定义函数的问题。导致了函数调用处代码复杂。
Think twice, code once? 我对这句话的理解是应该深思熟虑设计好架构后再开始code,但是并不意味着需要构思好所有的代码细节再开始动手,最重要的是有一种“自顶向下”的构建思路,由大的架构思考类的设计和类之间的关系,再确定类的成员、每个类的方法,最后考虑类似于数据类型、容器类型、方法实现等具体细节。在code过程中一定要注重动手实践,也许脑中构建的空中楼阁毫无实现思路,但是只要触碰到键盘,便“来了感觉“文思泉涌。而优秀的架构能够显著减少编程时的思维难度和代码量,简洁的代码架构也会很大程度减少tle的可能。
多多关注讨论区。讨论区的精华帖往往能够解决一些自己短时间内无法独立思考出的问题。没有思路时不妨参考一下优秀同学的思路。
性能和正确性有制衡关系。优化代码会提高性能分,但是会降低代码正确的概率以及增加代码的运行时间。最好的办法当然是采取又快又好的优化方案,但必要时舍弃部分性能分换取正确性也是不得不做的 tradeoff。
提高代码可扩展性,为可能的需求迭代预留空间。完成第一单元的代码时我便考虑到后续嵌套表达式的问题,采用了递归下降的方法解析表达式,避免了第二次作业的大规模重构,在第二次作业时创建的有关自定义函数的方法稍作修改便可用于第三次作业。也许提高可扩展性,为后续迭代开发预留空间的过程会come at a cost,但是这点小小的代价相比于大规模重构的痛苦还是来得太值得。
重视测试。测试不能证明正确性,但是能发现错误,测试出了错误就要好好调试好好优化,而非抱侥幸心理,还要针对新增的优化优化等部分做全面的测试。
在我痛苦的 coding 过程中,课程组的训练框架给了我极大的帮助,让我迅速上手了最为困难的递归下降法部分
在三层结构的计算过于抽象时,学长的博客给了我极大的帮助,Poly - Mono 的代码结构也给了我莫大的启发
感谢老师、各位助教、课程组以及研讨课上妙点频出的同学们,展示了 OO 这门课的风采
最后也想感谢一下坚持到现在的自己
Thanks so much! 多谢你哋!