一、程序结构
架构设计
在表达式解析上,我在第一次就采用了课程组推荐的递归下降,在之后的每次迭代中,只需要根据新需求更改 Lexer 获取 tokens 的方法,并为 Parser 类 parseFactor 方法增加新的因子的解析创建方法即可。
将 Expr, Term, Factor 统一视作多项式(Polynomial)。每个多项式由多个单项式(Monomial)相加减组成,而每个单项式可以视作系数乘以基本单元(Unit)组成,根据题目的表述,可以得到基本单元的下列定义:
x
a
⋅
exp
(
poly
)
x^a \cdot \exp(\text{poly})
xa⋅exp(poly)
public class Unit {
private final BigInteger exponentX;
private final Polynomial exponentE;
}
为了方便对多项式合并同类项,可以用一个 HashMap
表示多项式,key 是 Unit,value 是系数。
public class Polynomial {
private final HashMap<Unit, BigInteger> monomials;
}
因此,只需要为 Expr, Term, Factor 类都实现一个方法 toPolynomial
,将该类中的数据转化为一个多项式,就可以实现设计的统一:
- Expr 加入一个 Term,等价于进行多项式加法
- Term 加入一个 Factor,等价于进行多项式乘法
在 Polynomial 类内部需要进行复杂的数学运算(将数学意义上的加减乘运算转化为对HashMap元素的操作),为了实现层次化设计和抽象,我选择了将 Polynomial 类的计算细节尽量隐藏,暴露给外界的接口尽可能符合多项式计算的实际意义。
public class Polynomial {
public Polynomial addPolynomial(Polynomial addend);
public Polynomial subPolynomial(Polynomial subtrahend);
public Polynomial mulPolynomial(Polynomial multiplier);
public Polynomial mulConstant(BigInteger constant);
public Polynomial pow(int exponent);
}
但是在 Expr, Term, Factor 中,通过组合(Combination)的方式将多项式作为一个属性,但实际上表示 Expr, Term, Factor 只需要一个属性多项式,在最后总结时我认为可以考虑将 Polynomial 作为父类,起到类似与 Factor 的作用,但是这样做需要进行重构,修改类之间的关系。
复杂度分析
使用 MetricsReloader 分析代码复杂度后结果:
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
DeriveFactor.DeriveFactor(Expr) | 1 | 1 | 1 |
DeriveFactor.toPolynomial() | 1 | 1 | 1 |
ExpFactor.ExpFactor(Polynomial) | 1 | 1 | 1 |
ExpFactor.toPolynomial() | 1 | 1 | 1 |
ExpFactor.toString() | 1 | 1 | 1 |
Expr.Expr() | 1 | 1 | 1 |
Expr.Expr(ArrayList<Term>) | 1 | 1 | 1 |
Expr.addTerm(Term) | 1 | 1 | 1 |
Expr.toPolynomial() | 1 | 3 | 3 |
Expr.toString() | 1 | 1 | 1 |
ExprFactor.ExprFactor(Expr, int) | 1 | 1 | 1 |
ExprFactor.toPolynomial() | 1 | 1 | 1 |
ExprFactor.toString() | 1 | 1 | 1 |
ExprHandler.RemoveBlank(String) | 1 | 1 | 1 |
ExprHandler.RemoveExtraPlusMinus(String) | 1 | 5 | 6 |
ExprHandler.RemoveForeheadZero(String) | 1 | 1 | 1 |
FuncDefine.FuncDefine(String, ArrayList<String>) | 1 | 1 | 1 |
FuncDefine.call(ArrayList<Polynomial>) | 2 | 2 | 3 |
FuncDefine.funcBody() | 1 | 1 | 1 |
FuncFactor.FuncFactor(String, HashMap<String, Polynomial>) | 1 | 4 | 4 |
FuncFactor.toPolynomial() | 1 | 1 | 1 |
FuncFactor.toString() | 1 | 1 | 1 |
FuncManager.add(String, FuncDefine) | 1 | 1 | 1 |
FuncManager.get(String) | 1 | 1 | 1 |
FuncManager.recordFunc(String) | 1 | 2 | 2 |
Lexer.Lexer(String) | 1 | 18 | 18 |
Lexer.isEmpty() | 1 | 1 | 1 |
Lexer.next() | 1 | 1 | 1 |
Lexer.peek() | 1 | 1 | 1 |
Monomial.Monomial(String, Polynomial) | 1 | 1 | 1 |
Monomial.Monomial(String, String) | 1 | 1 | 1 |
Monomial.Monomial(String, Unit) | 1 | 1 | 1 |
Monomial.toString() | 5 | 5 | 5 |
Parser.Parser(Lexer) | 1 | 1 | 1 |
Parser.parseDeriveFactor() | 1 | 1 | 1 |
Parser.parseExpFactor() | 1 | 3 | 3 |
Parser.parseExpr() | 1 | 8 | 9 |
Parser.parseExprFactor() | 1 | 3 | 3 |
Parser.parseFactor() | 8 | 9 | 10 |
Parser.parseFuncFactor() | 1 | 3 | 3 |
Parser.parseNumber(int) | 1 | 1 | 1 |
Parser.parseTerm(int) | 1 | 3 | 3 |
Parser.parseVar() | 2 | 2 | 2 |
Polynomial.Polynomial() | 1 | 1 | 1 |
Polynomial.Polynomial(HashMap<Unit, BigInteger>) | 1 | 1 | 1 |
Polynomial.Polynomial(Monomial) | 1 | 1 | 1 |
Polynomial.Polynomial(Polynomial) | 1 | 1 | 1 |
Polynomial.addPolynomial(Polynomial) | 1 | 2 | 2 |
Polynomial.derive() | 1 | 2 | 2 |
Polynomial.equals(Object) | 3 | 5 | 7 |
Polynomial.gcdMultiple() | 3 | 3 | 4 |
Polynomial.getMonomials() | 1 | 1 | 1 |
Polynomial.hashCode() | 1 | 1 | 1 |
Polynomial.isZero() | 1 | 1 | 1 |
Polynomial.mulConstant(BigInteger) | 1 | 1 | 1 |
Polynomial.mulMonomial(Monomial) | 1 | 2 | 2 |
Polynomial.mulPolynomial(Polynomial) | 1 | 2 | 2 |
Polynomial.pow(int) | 2 | 2 | 3 |
Polynomial.simplify(BigInteger) | 2 | 3 | 3 |
Polynomial.size() | 1 | 1 | 1 |
Polynomial.subPolynomial(Polynomial) | 1 | 2 | 2 |
Polynomial.toString() | 2 | 7 | 7 |
Term.Term() | 1 | 1 | 1 |
Term.Term(ArrayList<Factor>, int) | 1 | 1 | 1 |
Term.addFactor(Factor) | 1 | 1 | 1 |
Term.toPolynomial() | 1 | 2 | 2 |
Term.toString() | 1 | 1 | 1 |
Unit.Unit(BigInteger, Polynomial) | 1 | 1 | 1 |
Unit.derive() | 1 | 1 | 1 |
Unit.deriveExp() | 1 | 1 | 1 |
Unit.deriveX() | 2 | 2 | 2 |
Unit.equals(Object) | 3 | 3 | 5 |
Unit.expForm() | 7 | 7 | 9 |
Unit.hashCode() | 1 | 1 | 1 |
Unit.multiply(Unit) | 1 | 1 | 1 |
Unit.simplifyExp() | 2 | 6 | 6 |
Unit.toString() | 4 | 5 | 6 |
Unit.xForm() | 2 | 1 | 2 |
-
Lexer 的构造函数具有很高的圈复杂度,这主要是因为在将字符串提取为 token 时,涉及到大量的分支判断并且全部都放在同一个方法内,面对新增需求会使得修改变得很复杂,可以考虑下面的方法来降低复杂度
- 将处理不同类型字符的逻辑分别抽取为私有方法
- 使用策略模式,定义一个Token处理策略接口,然后实现不同的Token处理策略类。在 Lexer 类中使用相应的策略来处理不同类型的Token,从而避免复杂的分支判断,同时也方便增加新需求
-
在 Parser 类的 parseFactor 方法中也出现了与 Lexer 构造函数类似的情况,不过在 parseFactor 方法对应的各个分支内的行为全部封装为了函数,面对这样多分支且每个分支需要创建不同对象的情况,在查找相关资料后我认为可以采用工厂模式提高程序的扩展性,在工厂中决定创建哪个对象的过程可以采用映射表,降低分支复杂度。
public interface Factor { void process(); } public class ExprFactor implements Factor { @Override public void process() { // 处理表达式因子的逻辑 } } // 其他 Factor 实现类... public class FactorFactory { private final Map<String, Supplier<Factor>> factoryMap; public FactorFactory() { this.factoryMap = new HashMap<>(); initializeFactoryMap(); } private void initializeFactoryMap() { factoryMap.put("(", ExprFactor::new); // 添加其他 Factor 实现类的映射... } public Factor createFactor(String type) { Supplier<Factor> supplier = factoryMap.get(type); if (supplier == null) { throw new IllegalArgumentException("Invalid factor type: " + type); } return supplier.get(); } } // 在调用处使用 FactorFactory 创建对象 public class Parser { private Lexer lexer; private FactorFactory facory; public Parser(Lexer lexer) { this.lexer = lexer; this.factory = new FactorFactory(); } public Factor parseFacotr() { return factory.createFactor(lexer.peek()); } }
-
在 Unit 类中输出指数部分的方法 expForm 具有较高的圈复杂度,这是由于为了较好的性能得分,需要考虑如何添加括号、提取因子等,带来了大量的分支条件判断和方法调用。在编写这段代码中,出现了部分bug,解决 bug 的方式是在一个分支中继续嵌套条件判断,使得代码可读性很差,可以考虑抽取出共同点,减少条件判断。
使用 statistics 工具分析 src 目录下代码行数结果:
由于 Polynomial 类实现了程序中的计算部分,具有较多的行数,但是 Polynomial 作为一个底层的抽象表示,在上层编写代码时一般不需要理解内部的实现细节,只需要调用计算接口,因此不会带来迭代和理解的复杂;Unit 类主要承担了基本项表示的功能,有很大一部分代码用来简化输出,在面对新的迭代需要,如增加三角函数因子时必须要修改 Unit 类,代码的规模可能会逐渐变大,并且不好维护,需要考虑将
x
a
x^a
xa,
exp
\exp
exp 从 Unit 中抽取,减少 Unit 类的复杂度。
二、迭代
第一次作业
第一次面对这样一个看起来很“数学”的问题,我无法将它与面向对象联系到一起,在看完了相关博客和递归下降后,我初步有了一个通过 Expr, Term, Factor 将表达式组织起来的想法,在初次完成代码时,我使用 Factor 作为一个接口,让 Expr, Term, ExprFactor 都实现了 Factor 接口,为了方便化简,需要用一个哈希表来通过 x x x 的次数 d e g r e e degree degree 获得对应的系数 coeffcient \text{coeffcient} coeffcient,借此机会,我想到了,是不是可以通过一个哈希表,将 Expr, Term, Factor 统一起来,而不是通过 Factor 这个接口,于是我认为所有的表达式,项,因子都可以通过一个多项式表示,这个多项式的数学含义如下:
poly
=
∑
coefficient
⋅
x
d
e
g
r
e
e
\text{poly} = \sum \text{coefficient} \cdot x^{degree}
poly=∑coefficient⋅xdegree
从代码的角度可以用一个以次数
degree
\text{degree}
degree 为 key,
coefficient
\text{coefficient}
coefficient 为 value 的 hashmap
来表示。
在我看来这样做有两个好处:每次 addTerm,addFactor 的过程已经实现了同类项的合并;相比于不含任何属性的接口,使用多项式这个包含了所有属性的类可以将底层的计算逻辑和解析逻辑分离,实现层次化设计。
第二次作业
第二次作业的两个需求:自定义函数和 exp \exp exp因子
首先需要记录一个函数的定义,由三部分组成:函数名,形参列表,函数体,这里我使用了 FuncDefine 表示一个函数的定义,为了方便获取和添加自定义函数,我使用了 FuncManager 来管理所有函数的定义。
为了实现自定义函数的计算,我创建了 FuncFactor 类,它实现了 Factor 接口,可以计算一个自定义函数在调用后的值并返回一个多项式。内部计算的具体细节是在 Parser 中获取函数名和实参,通过函数名获取 FuncDefine,然后 FuncDefine 的 call 方法将形参和实参一一对应,用实参替换函数体中的实参,得到可通过 parseExpr 方法计算的一个表达式。
![](https://i-blog.csdnimg.cn/blog_migrate/a6f5f329c1fabff5ec484477558137ec.png#pic_center)
我将
exp
\exp
exp 视作了和
x
a
x^a
xa 等价的一个因子,用一个类 Unit 表示二者的乘积,由 Unit 乘上系数得到单项式 Monomial,多个 Monomial 构成了一个多项式,于是可以这样表示一个多项式:
poly
=
∑
coefficient
⋅
Unit
\text{poly} = \sum \text{coefficient} \cdot \text{Unit}
poly=∑coefficient⋅Unit
从代码的角度可以使用一个以 Unit 为 key,Coefficient 为 value 的哈希表表示多项式。于是,我需要重写 Unit 的 equals 和 hashcode 方法来实现 hashmap,并且为了方便输出需要重写 toString 方法。
public class Unit {
private final BigInteger exponentX;
private final Polynomial exponentE;
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
Unit other = (Unit) obj;
return this.exponentX.equals(other.exponentX) && this.exponentE.equals(other.exponentE);
}
@Override
public int hashCode() {
return Objects.hash(exponentX, exponentE);
}
}
第三次作业
第三次作业的两个需求:自定义函数的嵌套定义和求导因子
第二次作业对自定义函数的处理方法仍适用于嵌套定义的递归函数,因此第一个需求不需要做出任何更改。
第二个需求的求导因子根据 Polynomial 的统一表示方法,只需要为 Polynomial 实现一个 Derivable 接口,其中需要实现 derive 方法,该方法会返回对当前多项式求导后的结果(一个多项式),值得注意的是求导过程中出现的拷贝问题。
对
exp
(
poly
)
\exp(\text{poly})
exp(poly) 求导的结果是
exp
(
poly
)
⋅
poly
′
\exp(\text{poly}) \cdot \text{poly}'
exp(poly)⋅poly′,从多项式的角度看相当于先复制了一份
exp
(
poly
)
\exp(\text{poly})
exp(poly), 然后乘上
poly
′
\text{poly}'
poly′。这一过程可能会出现浅拷贝带来的对象地址问题,在我的代码中,多项式的每种计算都返回一个新的多项式对象,并且 Unit,Polynomial 的属性都是 final
修饰的,所以不需要考虑拷贝的问题。
三、测试方法
单元测试
在第一次作业完成 Polynomial 类和第三次作业实现 Derivable 接口中,使用 JUnit 进行单元测试提高了我的开发效率。例如,在第一次作业中,我需要先完成 Polynomial 类中的计算方法,并且检验正确性,之后在 Expr, Term, Factor 中进行调用。JUnit 的单元测试为我提供了很方便的工具可以在开发过程中检验某个类的正确性,这样在 debug 过程中,在进行充分测试的情况下,我可以跳过 Polynomial 的方法,更快定位错误。
以 Polynomial 中的 addPolynomial 类为例:
/**
* 将该多项式加上另一个多项式
* @param addend 要相加的多项式
* @return this + addend
*/
public Polynomial addPolynomial(Polynomial addend) {
HashMap<Unit, BigInteger> result = new HashMap<>(this.monomials);
addend.monomials.forEach((key, value) -> {
BigInteger resCoefficient = result.getOrDefault(key, BigInteger.ZERO).add(value);
if (resCoefficient.equals(BigInteger.ZERO)) {
result.remove(key);
} else {
result.put(key, resCoefficient);
}
});
return new Polynomial(result);
}
为了进行充分测试,我从代码的分支结构和多项式加法本身意义两个角度构造了测试点。
- 第一个条件分支: this = 2 x , addend = − 2 x \text{this}=2x, \text{addend}=-2x this=2x,addend=−2x
- 第二个条件分支: this = 2 x , addend = − x 2 \text{this}=2x, \text{addend}=-x^2 this=2x,addend=−x2
- 分别经过两个条件分支: this = 2 x − 1 , addend = − x 2 \text{this}=2x-1, \text{addend}=-x^2 this=2x−1,addend=−x2
- 两个值相反的多项式相加: this = 2 x − 1 , addend = 1 − 2 x \text{this}=2x-1, \text{addend}=1-2x this=2x−1,addend=1−2x
- 加上一个0(特殊测试点): this = 2 x − 1 , addend = 0 \text{this}=2x-1, \text{addend} = 0 this=2x−1,addend=0
public class PolynomialTest {
@Test
public void testAddPolynomialWithZeroPolynomial() {
Polynomial p = new Polynomial();
Polynomial result = p.addPolynomial(new Polynomial());
assertEquals(p, result);
}
@Test
public void testAddPolynomialWithSameDegree() {
Polynomial p1 = new Polynomial();
p1.addMonomial(2, BigInteger.valueOf(3)); // Assuming there's an addMonomial method
Polynomial p2 = new Polynomial();
p2.addMonomial(2, BigInteger.valueOf(5));
Polynomial result = p1.addPolynomial(p2);
assertEquals(new BigInteger("8"), result.getMonomialCoefficient(2)); // Assuming there's a getMonomialCoefficient method
}
@Test
public void testAddPolynomialWithDifferentDegrees() {
Polynomial p1 = new Polynomial();
p1.addMonomial(2, BigInteger.valueOf(3));
Polynomial p2 = new Polynomial();
p2.addMonomial(3, BigInteger.valueOf(4));
Polynomial result = p1.addPolynomial(p2);
assertEquals(BigInteger.valueOf(3), result.getMonomialCoefficient(2));
assertEquals(BigInteger.valueOf(4), result.getMonomialCoefficient(3));
}
}
在第三次作业中实现 Derivable 接口的求导方法,在求导过程中,需要对每个单项式分别求导再将结果通过 addPolynomial 方法相加,我从求导本身意义上构造了测试点:
- 对一个常数求导(特别测试对0, exp ( c ) \exp(c) exp(c)求导)
- 对 a x b ax^b axb 形式 与 exp ( a x b ) \exp(ax^b) exp(axb) 形式求导
- 对 a x b × exp ( c x d ) ax^b \times \exp(cx^d) axb×exp(cxd) 形式求导
- 对 exp ( Poly ) \exp(\text{Poly}) exp(Poly) 形式求导
class PolynomialTest {
@Test
void testDeriveZero() {
Polynomial zero = new Polynomial();
Polynomial derivedZero = zero.derive();
assertTrue(derivedZero.isZero(), "Deriving zero polynomial should result in zero polynomial.");
}
@Test
void testDeriveConstant() {
Polynomial constant = new Polynomial().addMonomial(new Unit(0), BigInteger.valueOf(5));
Polynomial derivedConstant = constant.derive();
assertTrue(derivedConstant.isZero(), "Deriving a constant polynomial should result in zero polynomial.");
}
@Test
void testDeriveLinear() {
Polynomial linear = new Polynomial().addMonomial(new Unit(1), BigInteger.valueOf(3));
Polynomial derivedLinear = linear.derive();
assertEquals(new Polynomial().addMonomial(new Unit(0), BigInteger.valueOf(3)), derivedLinear, "Deriving a linear polynomial should result in a constant polynomial.");
}
@Test
void testDeriveQuadratic() {
Polynomial quadratic = new Polynomial()
.addMonomial(new Unit(2), BigInteger.valueOf(2))
.addMonomial(new Unit(1), BigInteger.valueOf(3));
Polynomial derivedQuadratic = quadratic.derive();
assertEquals(new Polynomial().addMonomial(new Unit(1), BigInteger.valueOf(6)), derivedQuadratic, "Deriving a quadratic polynomial should result in a linear polynomial.");
}
}
数据生成器与评测机
单元测试适用于检查某个方法的正确性,但是无法充分地检查所有情况,并且对于因为类之间的聚合调用产生的 bug 无法很好地检查。因此我根据形式化表述用 Python 实现了数据生成器,并调用 sympy 库进行了值的正确性检验,但是无法验证结果的结构是否满足题目要求,如 exp ( x ) − 1 \exp(x)^{-1} exp(x)−1 这样的错误形式无法检查。下面是评测机代码仓库:Unit1 评测机
四、程序bug分析
在第二次作业中,Unit 类的 expForm 方法在 exp ( mono ) \exp(\text{mono}) exp(mono)(mono为一个单项式)的输出中出现了bug,原因在减少括号层数时,在处理形如 exp ( ( − 2 × x ) ) \exp((-2\times x)) exp((−2×x))时,会输出 exp ( x ) − 2 \exp(x)^{-2} exp(x)−2,这违背了题目的格式;另一方面,在输出形如 exp ( x 2 × exp ( 1 ) ) \exp(x^2\times \exp(1)) exp(x2×exp(1))时,会输出 exp ( x × exp ( 1 ) ) \exp(x\times \exp(1)) exp(x×exp(1))。
下面是出现 bug 的方法,可以看到方法中出现了大量条件、循环的嵌套,特别是出现了高达三层的条件嵌套处理!于是在需要考虑多种情况的化简输出中,忽略了很多情况,这导致了上面的第一个错误,其次数在写代码是对当前分支下处理的情况经常弄混,这导致上面的第二个错误。另一方面,没有使用 JUnit 工具进行单元测试,过于相信自己的代码也导致了第二次强测中出现了两个错误,在互测中被抓住这个方法的错误疯狂hack。
private String expForm() {
if (exponentE.size() == 1) {
for (Map.Entry<Unit, BigInteger> entry : exponentE.getMonomials().entrySet()) {
Unit unit = entry.getKey();
BigInteger coefficient = entry.getValue();
if (unit.equals(Unit.ONE)) {
return "exp(" + exponentE + ")";
} else if (unit.exponentX.equals(BigInteger.ZERO)) {
if (coefficient.equals(BigInteger.ONE)) {
return "exp(" + exponentE + ")";
} else {
return "exp(" + unit.expForm() + ")^" + coefficient;
}
} else {
if (coefficient.equals(BigInteger.ONE) && unit.exponentE.equals(Polynomial.ZERO)) {
return "exp(" + exponentE + ")";
} else {
return "exp(" + unit.xForm() + ")^" + coefficient;
}
}
}
}
return simplifyExp();
}
该方法的复杂度分析结果如下:
method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Unit.expForm() | 7 | 8 | 8 |
在修改后,一方面我通过调整条件判断顺序,减少了分支嵌套,另一方面,提取条件分支中的共同部分,这样,在加入对负系数处理后依然能够保证最多只有两层的分支嵌套。为了更好的降低圈复杂度,应该考虑将第二个分支条件中的三个分支合并抽取为一个方法。
private String expForm() {
if (exponentE.size() != 1) {
return simplifyExp();
}
Map.Entry<Unit, BigInteger> entry = exponentE.getMonomials().entrySet().iterator().next();
Unit unit = entry.getKey();
BigInteger coefficient = entry.getValue();
if (unit.equals(Unit.ONE)) {
return "exp(" + exponentE + ")";
} else if (unit.exponentX.equals(BigInteger.ZERO) || unit.exponentE.equals(Polynomial.ZERO)) {
if (coefficient.equals(BigInteger.ONE)) {
return "exp(" + exponentE + ")";
} else if (coefficient.signum() == -1) {
return "exp((" + exponentE + "))";
} else {
return "exp(" + unit + ")^" + coefficient;
}
} else {
if (coefficient.equals(BigInteger.ONE) || coefficient.signum() == -1) {
return "exp((" + exponentE + "))";
} else {
return "exp((" + unit + "))^" + coefficient;
}
}
}
五、优化策略
在本单元中,我主要进行了下面三个方面的优化:
- 去除多余的正负号
- 调整一个多项式中的加减号位置,如 -1+x 可变为 x-1
- 简化
exp
\exp
exp项的输出,记
exp
(
poly
)
\exp(\text{poly})
exp(poly)内部的表达式为
poly
\text{poly}
poly
- 若
poly
\text{poly}
poly 只有一项,记为
poly
=
c
⋅
x
a
exp
(
p
)
\text{poly} = c\cdot x^a \exp(p)
poly=c⋅xaexp(p)
- 若 c 为正数,有下面优化方式
若a为0且p为0,输出 exp(c) 若a为0且p不为0,输出 exp(exp(p))^c 若a不为0且p为0,输出 exp(x^a)^c 若a和b都不为0,输出exp((x^a*exp(p)))^c
- 若 c 为负数,有下面优化方式
若a为0且p为0,输出 exp(c) 若a为0且p不为0,输出 exp((c*exp(p))) 若a不为0且p为0,输出 exp((c*x^a)) 若a和b都不为0,输出exp((c*x^a*exp(p)))
- 若 c 为正数,有下面优化方式
- 若 poly \text{poly} poly有多项,考虑提取公因数(常数),记 gcd \text{gcd} gcd 为 poly \text{poly} poly 中每个单项式的系数的最大公因数,那么可以从取 p p p从1到10,选 q = g c d p q = \frac{gcd}{p} q=pgcd 为公因子,将多项式化为 poly = poly ′ × q \text{poly} = \text{poly}' \times q poly=poly′×q,输出 exp ( poly ′ ) q \exp(\text{poly}')^{q} exp(poly′)q。
- 若
poly
\text{poly}
poly 只有一项,记为
poly
=
c
⋅
x
a
exp
(
p
)
\text{poly} = c\cdot x^a \exp(p)
poly=c⋅xaexp(p)
第一项和第二项优化的实现比较简单,但是在进行第三项优化中 poly \text{poly} poly 只有一项的情况时,复杂的条件判断使得 Unit 类的对应方法变得极为臃肿,但是该优化只在 Unit 类的 toString 方法中被调用,并没有对整体的架构产生影响。为了让优化不影响整体架构和设计,应该将与优化相关的函数单独封装,并尽可能只在输出部分(toString方法)调用,优化不能对类本身的状态造成影响。
六、感想与建议
心得与收获
由于 pre 课程已经让我初步认识到了面向对象设计层次化设计的思想,因此第一单元在花时间理解了递归下降后对我而言主要的难点就是如何优雅地统一 Expr, Term, Factor 这三个类,最终我也比较顺利地完成了三次作业,在过程中并没有进行重构。
遗憾的是,Parser 类的处理过于丑陋,没有考虑将工厂模式应用到不同 Factor 对象的创建上,在优化上,也只是做了一些简单的处理并且考虑不够精细。
在编码过程中,掌握了一些 Java 的语法,如 lambda 表达式,Stream 等,这些方法在保证代码可读性的同时有效地减少了代码的长度。在进行字符串处理时,使用正则表达式可以有效地减少 if 语句的出现,提高程序的可读性。
在开始新的一次迭代时,可以从顶层(如 Parser)开始分析需要增加的功能,然后从顶层的需要实现的新功能出发,得到底层的类(如 Polynomial 和 Unit)类新增的方法。在编写代码时,可以从底层出发,在完成了相应代码编写后,使用 JUnit 进行单元测试,保证底层方法的正确性再完成那些需要调用刚测试完的方法的代码。
相比于 pre 课程,我在第一单元开始更加关注程序的结构,会尽量使得自己的程序面对新需求时可以符合开闭原则进行迭代。另一方面,我开始意识到一份好的面向对象代码不一定是最短的代码,不应该过度追求写出短的代码而抛弃了设计和可读性,例如,使用工厂模式比起直接分不同情况创建不同对象写起代码来一定行数更多更麻烦,但是这样可以获得更好的扩展性。
建议
- 性能分的分布可以采取按区间分配或者按照比例分配
- 希望在指导书中可以给出更多的参考资料,在作业完成后课程组发布一些真正好的架构
- 为了提高程序的可扩展性,表达式中使用的变量可以由控制台输入,而不是提前规定好直接使用 x , y , z x,y,z x,y,z 作为变量