301
社区成员
发帖
与我相关
我的任务
分享这是本学期OO课程的第一次作业。得益于上学期上过的OOpre,第一单元的工作相对来说较为容易。虽然OOPre喜提一次强测0分
第一单元的总体任务是对表达式进行化简,之后加了对指数函数、多重括号嵌套、自定义函数与求导的迭代开发。
本次作业的UML类图如下:

解析类负责将已处理的表达式字符串进行解析得到Expr对象,再由Expr对象通过计算转化为Poly对象,最后调用Poly对象的输出方法进行输出。
表达式可以解析为Expr、Term和Factor三级,其中Factor可分为幂函数、常数因子、表达式因子三类。Factor的行为抽象层次重合度高,所以我们可以引入Factor接口,并使用NumFactor、PowFactor和Expr三个类来实现。
Expr类中使用ArrayList来容纳该表达式中含有的Term,Term类中使用ArrayList来容纳该项中含有的Factor。
//Factor.java
public interface Factor {
//......
}
//Expr.java
public class Expr implements Factor {
//......
}
//NumFactor.java
public class NumFactor implements Factor {
//......
}
//PowFactor.java
public class PowFactor implements Factor {
//......
}
对输入的表达式进行解析,我能想到的有正则表达式和递归下降法两种方法。越读了一些学长学姐的博客,他们都选择了递归下降法,所以我在作业中也采用的是递归下降法。
对于递归下降解析表达式并进行化简的方法,我大概分成了三大板块——字符串处理、表达式解析、计算化简。
字符串处理
在这部分我没有做太多的工作,主要是看到有些博客中提到进行处理时或多或少都碰到一些奇奇怪怪的bug,从而导致强测互测起飞的故事。
对于输入的字符串,我使用Pretreat类进行处理。
// Pretreat.java
public class Pretreat {
private void delWhite() {
// 删除表达式中的所有空格
}
private void delExpSymbol() {
// 删除指数可能带有的正号
// 这是我在中测时发现的bug,图省事就直接把"^+"变为"^"
}
}
对于输出的字符串,使用Simplify类。
// Simplify.java
public class Simplify {
private void delAddSub() {
// 删除或转换连续的正负号
// 例如,把"+-"替换为"-"
}
}
我在本单元的三次作业都采用了以上两个工具类。最初的设想是为了化简表达式,让字符串变短,所以设计了两个类专门处理,但是发现可优化的部分并不多,现在看来可以统一成一个MyIOHandle类似乎就可以了。
表达式解析
结合公众号的相关分享,我使用了Lexer和Parser类对语法单元Token进行解析。为了方便存储语法单元的基本信息,我也为此专门设计了一个Token类。
// Token.java
public class Token {
public enum Type {
EOF, ADD, SUB, MUL, LPAREN, RPAREN, NUM, VAR, POW
}
private final Type type;
private final String content;
public Token(Type type, String content) {
this.type = type;
this.content = content;
}
public Type getType() {
return this.type;
}
public String getContent() {
return this.content;
}
}
结合Token类可实现词法分析的Lexer类:
// Lexer.java
import java.util.ArrayList;
public class Lexer {
private final ArrayList<Token> tokens = new ArrayList<>();
private String inputString;
private int pos = 0;
private final int length;
private int tokenspos = 0;
private final int tokenslength;
public Lexer(String inputString) {
// ……
while (pos < length) {
c = inputString.charAt(pos);
if (c == '(') {
tokens.add(new Token(Token.Type.LPAREN, "("));
} else if (c == ')') {
tokens.add(new Token(Token.Type.RPAREN, ")"));
} else if (c == '+' || c == '-') {
// ……
} else if (c == '*') {
// ……
} else if (c == '^') {
// ……
} else if (Character.isLetter(c)) {
// ……
} else if (Character.isDigit(c)) {
// ……
}
pos++;
}
tokenslength = tokens.size();
}
private boolean notEnd() {
// Token读完没?
}
public Token now() {
// 返回当前位置的Token对象
}
public void next() {
// 指向下一个Token
}
}
为了实现低耦合的目标,我在Lexer类里构造了专门解析特定单元的方法。以读取数字为例:
// Lexer.java
private String getNumber() {
String numString;
// 第一步:把所有的数字字符读取出来
// 第二步:删除所有的前导0
// 注意检查数字恰好为0是numString是否为空串,若是就补充一个'0'
return numString;
}
值得一提的是,很多同学处理连续正负号的时候,都是采用简单的字符串替换的方式。我采用了一种不同的方法。在读取到一个正负号时,我会尝试读取后面的字符判断是否为正负号,并更新整体的正负性。
Parser类的设计基本上沿用了实验课和公众号文章的整体思路,总体上分为parseExpr()、parseTerm()和parseFactor()三类。最后的结构是parseExpr()调用parseTerm(),parseTerm()调用 parseFactor(),parseFactor()调用parseExpr()。
// Parser.java
public class Parser {
public Expr parseExpr() {
Expr expr = new Expr();
// 判断整个表达式前是否带负号,并赋给第一个Term
expr.addTerm(parseTerm(sign));
while (lexer.now().getType() == Token.Type.ADD
|| lexer.now().getType() == Token.Type.SUB) {
// 处理Term的符号
expr.addTerm(parseTerm(sign));
}
return expr;
}
public Term parseTerm(int sign) {
Term term = new Term(sign);
// sign表示整个项的正负性
term.addFactors(parseFactor());
while (lexer.now().getType() == Token.Type.MUL) {
// 解析因子
// 假设出现"3 * -2"的情况,“*”的下一个Token是“-”
// 可以通过塞入因子“-1”的形式,事实上转换为"3 * -1 * 2"
// ……
}
return term;
}
public Factor parseFactor() {
if (lexer.now().getType() == Token.Type.LPAREN) {
// 解析表达式因子
} else if (lexer.now().getType() == Token.Type.VAR) {
// 解析PowFactor
} else {
// 解析NumFactor
}
}
}
计算化简
观察最底层的Factor类,可以将其数学表示抽象为
$$
PowFactor = a x^b
$$
$$
NumFactor = number = number * x^0
$$
那么,Term与Expr对象实际上都可以化简成
$$
\sum coef * x^{index}
$$
的形式。
数学形式相近,我们可以由此建立BasicUnit基本单元类和Poly多项式类。Poly是多个BasicUnit的和。
// BasicUnit.java
public class BasicUnit {
private BigInteger coef;
private final int exp;
private final int sign; // 整体的正负号
@Override
public String toString() {
// 输出为"a*x^b"的形式,可化简优化
}
public BasicUnit negate() {
// 相反项
}
public void add(BasicUnit other) {
// 加法运算
}
public BasicUnit mul(BasicUnit other) {
// 乘法运算
}
}
Poly类使用ArrayList来存储BasicUnit。在Poly类实现多项式的加法、乘法与乘方运算,同时重写toString()方法。
import java.util.ArrayList;
import java.util.Collections;
public class Poly {
private ArrayList<BasicUnit> basicUnits = new ArrayList<>();
// ……
public Poly addPoly(Poly other) {
// 加法运算
// 在这里可以合并指数相同的项
}
public Poly mulPoly(Poly other) {
// 乘法运算
}
public Poly powPoly(int exp) {
// 乘方运算
}
@Override
public String toString() {
}
}
然后,在Expr等类里均实现一个toPoly()方法。
对于NumFactor和PowFactor,只需要返回一个只含有一个BasicUnit的Poly对象。Term类把所有Factor.toPoly()的Poly对象相乘,Expr类把所有Term.toPoly()类相加。
最后调用Poly的toString方法,我们就得到答案了!
输出BasicUnit减少不必要的符号输出,如1,0,-x等。
对于Poly.toString()得到的输出,由于每个BasicUnit均由+连接,需要将+-转化为-。
最终输出时优先输出正数,如-x+1可以变为1-x
中强测、互测均未出现bug。
调试的时候出现了一些bug,比如:
x^0+0*x^3输出为0+0,判断合并上出现失误,最后通过在最后toString之前清除所有的0项解决。
^ +0003解析不到位
这次作业新增了指数函数与自定义函数的需求。
本次作业的UML类图如下:

本次迭代新增了IOHandle,ExpFactor和FuncFactor类。IOHandle类专门处理了字符串的输入与输出。
过程不再赘述,主要分享一下迭代新增的内容。
调用函数
为了便于自定义函数的定义和解析,本次作业新建了一个工具类FuncDefiner,主要处理自定义函数的定义和调用。该函数的成员和方法都是静态的,意味着不需要实例化对象,直接通过类名即可调用。
// FuncDefiner.java
public class FuncDefiner {
// 通过函数名称获得函数定义式
private static HashMap<String, String> funcs = new HashMap<>();
// 通过函数名称获得形参列表
private static HashMap<String, ArrayList<String>> args = new HashMap<>();
public static HashMap<String, ArrayList<String>> getArgs() {
return args;
}
public static HashMap<String, String> getFuncs() {
return funcs;
}
public static void addFunc(String funcDefineString) {
// 解析函数表达式并更新HashMap
}
public static String callFunc(String funcName, ArrayList<Factor> paras) {
// 函数调用时使用
// 根据funcName获取形参列表,由形参列表与实参列表的对应关系
// 将函数定义式中的形参替换成实参的字符串形式
// 为了防止替换时出现问题,可以多套一些括号
}
}
表达式解析
在Parser类增加parseExpFactor()和pareseFuncFactor()两个方法。相对应的修改parseFactor()的方法。
// Parser.java
public Factor parseExpFactor() {
// 解析Expr
// 解析指数函数的幂
}
public Factor parseFuncFactor() {
ArrayList<Factor> paras = new ArrayList<>();
// 注意替换逗号时出现不必要的替换
String[] list = tempString.split(",");
for (String single : list) {
// 解析逗号分割的实参,转换成Expr对象
}
// ……
return new FuncFactor(name, paras);
}
计算化简
这次迭代,Expr对象可转化成
$$
\sum coef * x^{index} * exp(F(x))
$$
的形式。其中F(x)可以是一个Expr转化成的Poly。所以对BasicUnit进行一些适当的修改,然后重写一下计算方法,答案就出来了。
本次作业优化方向主要有两个:exp内部括号层数与提取公因数。
括号层数控制
假设exp()括号内的部分是一个长的像因子的Expr,比如1,x^12之类,输出的时候可以输出为exp(x^12),而不必输出为exp((x^12))。那么,怎么判断一个Expr是一个长的像因子的Expr呢?
我在BasicUnit类和Poly类都实现了相关的方法:
// BasicUnit.java
public String toOrdExpString() {
StringBuilder sb = new StringBuilder();
sb.append("exp(");
if (poly.isZero()) {
// "1"
} else if (poly.isFactor()) {
// poly是F(x)转化成Poly的结果
// 只需一层括号
} else {
// 需两层括号
}
return sb.toString();
}
// Poly.java
public boolean isFactor() {
if (isZero()) {
// 必然的
} else if (basicUnits.size() > 1) {
// poly的项大于1,显然不可能像Factor
}
// 到这,basicUnits.size() == 1
BasicUnit unit = basicUnits.get(0);
// 可能符合要求的情况有:
// 1. unit为0
// 2. unit为"常数"
// 3. unit为"x^指数"
// 4. unit为exp(一堆东西),整体来看是一个exp因子
}
同时,判断是否可以合并需要判断F(x)是否相等,可以通过以下的方式实现:
// BasicUnit.java
public boolean isSame(BasicUnit other) {
return this.index.compareTo(other.index) == 0 &&
this.poly.isSame(other.poly);
}
// Poly.java
public boolean isSame(Poly other) {
// 全非空,为真
// 基本项的个数不同,为假
int fitCount = 0;
for (BasicUnit ba : this.basicUnits) {
for (BasicUnit bb : other.basicUnits) {
// 判断两个BasicUnit相等
}
}
// this.basicUnits的项均可以对应到相等的基本项,为真
}
提取公因数
遍历寻找Poly对象中每一个基本项系数的最大公因数。
如果优化效果达到比一般方法更长,则舍弃该优化。这样就可以保证,即使长度一样,也保守地采用原来的化简形式。
// BasicUnit.java
public String toExpString() {
String sim = toSimExpString(); // 尝试提取公因数
String ord = toOrdExpString();
if (//sim提取出了非法公因数) {
return ord;
} else if (//sim长度更短) {
return sim;
}
return ord;
}
中测、强测、互测均无出现bug。
hack阶段找到了三个人的问题,问题出在exp((-2 * x))上,他们提取得到了错误的结果exp(x)^-2。
当然,强测的性能分没有拿满。我也不知道怎么像优化仙人一样优化了
这次作业新增了求导因子的需求。
本次作业的UML类图如下:

本次设计与第二次作业相比,只添加了DxFactor类。
深拷贝
为了防止不必要的对象属性更改,我在本次作业中对Poly类和BasicUnit类实现了序列化深拷贝的方法。
// Poly.java
public class Poly implements Serializable {
private ArrayList<BasicUnit> basicUnits = new ArrayList<>();
public Poly getSame() {
Poly newPoly = null;
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bout);
out.writeObject(this);
ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream in = new ObjectInputStream(bin);
newPoly = (Poly) in.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return newPoly;
}
}
求导因子
求导因子的形式为dx(表达式),可以以类似于处理ExpFactor的方式来处理。
// Parser.java
public Factor parseDxFactor() {
lexer.next();
// 处理表达式
return new DxFactor(expr);
}
计算化简
我们可以对Expr、Term、Factor均实现toDiffPoly()方法,以实现表达式求导。
Term对象是多个Factor相乘的结果,所以求导的时候,要采用链式求导的方式。
// Term.java
public Poly toDiffPoly() {
Poly result = new Poly();
for (Factor f1 : factors) {
// 对f1求导
for (Factor f2 : factors) {
// f2原式与f1相乘
// 注意需要判断f1与f2是否相等
}
// 把结果加到result里
}
// ……
return result;
}
对于转化得到的Poly和BasicUnit对象,我们都需要分别实现求导方法。
我们知道BasicUnit的形式可以表示为:
$$
F(x) = ax^be^{g(x)}
$$
那么对BasicUnit求导可以得到一个Poly。
$$
F^{'}(x) = a[bx^{b-1}e^{g(x)} + g^{'}(x)x^be^{g(x)}]
$$
对Poly求导只需要对Poly所存的BasicUnit求导后相加即可。
// BasicUnit.java
public Poly diff() {
// 按照上面的公式计算
return result;
}
// Poly.java
public Poly diff() {
Poly result = new Poly();
for (BasicUnit unit : basicUnits) {
Poly aft = unit.diff();
result = result.addPoly(aft);
}
return result;
}
中测、强测、互测均无出现bug,hack阶段又遇到了全0房。
强测的性能分还是没有拿满。我也不知道怎么像优化仙人一样优化了
假设下一次作业需要迭代,比如说添加多变量因子x、y、z,那么应该怎么做呢?
首先,整个BasicUnit要改为
$$
F(x) = ax^by^cz^de^{g(x)}
$$
的形式。
对于BasicUnit之间加法与乘法的实现没有太大的难度,在求导上实现会稍微麻烦一些。
对于PowFactor,需要捕捉每一项因子的变量名是xyz当中的哪一个。
public class PowFactor implements Factor {
private char var;
private BigInteger index;
public PowFactor(char var, BigInteger index) {
this.var = var;
this.index = index;
}
@Override
public Poly toPoly() {
// 把var和index传入构建BasicUnit和Poly
}
}
总体来看,需要迭代的部分不是很多。
最终代码的规模如下:
| Source File | Total Lines | Source Code Lines |
|---|---|---|
| BasicUnit.java | 206 | 185 |
| DxFactor.java | 16 | 13 |
| ExpFactor.java | 31 | 27 |
| Expr.java | 34 | 28 |
| Factor.java | 5 | 4 |
| FuncDefiner.java | 78 | 71 |
| FuncFactor.java | 21 | 17 |
| IOhandle.java | 22 | 18 |
| Lexer.java | 147 | 137 |
| MainClass.java | 15 | 15 |
| NumFactor.java | 21 | 17 |
| Parser.java | 151 | 141 |
| Poly.java | 200 | 185 |
| PowFactor.java | 25 | 21 |
| Pretreat.java | 23 | 19 |
| Simplify.java | 19 | 16 |
| Term.java | 56 | 49 |
| Token.java | 21 | 17 |
| Total: | 1091 | 980 |
最终代码的复杂度如下。
| class | OCavg | OCmax | WMC |
|---|---|---|---|
| Token.Type | 0.0 | ||
| DxFactor | 1.0 | 1.0 | 3.0 |
| FuncFactor | 1.0 | 1.0 | 3.0 |
| IOhandle | 1.0 | 1.0 | 3.0 |
| NumFactor | 1.0 | 1.0 | 3.0 |
| PowFactor | 1.0 | 1.0 | 3.0 |
| Pretreat | 1.0 | 1.0 | 4.0 |
| Simplify | 1.0 | 1.0 | 3.0 |
| Token | 1.0 | 1.0 | 3.0 |
| Expr | 1.2 | 2.0 | 6.0 |
| MainClass | 2.0 | 2.0 | 2.0 |
| ExpFactor | 1.6666666666666667 | 3.0 | 5.0 |
| Term | 2.0 | 5.0 | 12.0 |
| FuncDefiner | 2.8 | 6.0 | 14.0 |
| Parser | 3.4444444444444446 | 7.0 | 31.0 |
| Poly | 3.4285714285714284 | 7.0 | 48.0 |
| BasicUnit | 2.0 | 10.0 | 40.0 |
| Lexer | 3.7777777777777777 | 12.0 | 34.0 |
| Total | 217.0 | ||
| Average | 2.2371134020618557 | 3.6470588235294117 | 12.055555555555555 |
复杂度比较高的方法如下:
| method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Parser.switchComma(String) | 19.0 | 1.0 | 6.0 | 10.0 |
| BasicUnit.toString() | 16.0 | 3.0 | 9.0 | 12.0 |
| Lexer.Lexer(String) | 14.0 | 12.0 | 12.0 | 15.0 |
| Poly.isSame(Poly) | 11.0 | 4.0 | 7.0 | 9.0 |
| Parser.parseFactor() | 9.0 | 7.0 | 7.0 | 7.0 |
| Poly.isFactor() | 9.0 | 6.0 | 8.0 | 9.0 |
| Lexer.getSign(char) | 7.0 | 2.0 | 2.0 | 6.0 |
| Parser.parseExpr() | 7.0 | 1.0 | 7.0 | 7.0 |
| Term.toDiffPoly() | 7.0 | 4.0 | 4.0 | 5.0 |
| Lexer.getNumber() | 6.0 | 3.0 | 3.0 | 6.0 |
| Poly.searchPositive() | 6.0 | 4.0 | 4.0 | 4.0 |
CogC 是圈复杂度的意思,与下面的v(G)一样。 v(G) 是圈复杂度,表示程序中独立路径的数量,也就是测试程序所需的最少路径条数。圈复杂度越大,说明程序越复杂,越容易出错,越难测试和维护。 iv(G) 是模块设计复杂度,表示程序中结构化程度的高低。模块设计复杂度越大,说明程序越不符合结构化设计原则,越难理解和修改。 ev(G) 是基本复杂度,表示程序中非结构化成分的多少。非结构化成分指的是那些不遵循顺序、选择、循环三种基本控制结构的语句或语句块。基本复杂度越大,说明程序越不规范,越降低了代码质量和可读性。
因为三次迭代都没有进行过重构,所以上面许多方法的基本复杂度还是挺糟糕的,这也说明了我的代码质量与风格确实有比较大的问题。比如说Lexer.Lexer(String)和Poly.isSame(Poly)存在许多的特判与递归调用等,可以对此进行一些改进。
首先先拿评测机进行无差别攻击压力测试,如果发现错误了,该怎么做呢?
我们可以将评测机生成的用例进行拆分,分别进行测试。
找到错误后开始读源码,在源码定位到错误之后,就可以构造符合要求的hack数据了!
这次作业让我深刻体会到了良好架构的重要性,由于前期设计的比较完善,后面的几次迭代任务都可以很清楚且简单的完成。当然,有些方法的耦合度和复杂度都还比较高,这说明我的面向对象设计的能力还存在一些不足。
在写代码之外,我还明白了一些道理,比如说一定要放手去做,多去实践才能发现到最优解。