OO_Lab0
问题描述
对表达式结构进行建模,将表达式中非必要的括号进行展开并化简。
设定的形式化表述(仅写出部分):
表达式 \(\rightarrow\) 空白项 [加减 空白项] 项 空白项 | 表达式 加减 空白项 项 空白项
项 \(\rightarrow\) [加减 空白项] 因子 | 项 空白项 '*' 空白项 因子
因子 \(\rightarrow\) 变量因子 | 常数因子 | 表达式因子
变量因子 \(\rightarrow\) 幂函数 | 三角函数 | 自定义函数调用 | 求和函数
表达式因子 \(\rightarrow\) '(' 表达式 ')' [空白项 指数]
三角函数 \(\rightarrow\) 'sin' 空白项 '(' 空白项 因子 空白项 ')' [空白项 指数] | 'cos' 空白项 '(' 空白项 因子 空白项 ')' [空白项 指数]
解决思路
整个问题的解决可以分成两部分,一是从字符串分析出表达式结构并通过合适的方式保存,二是将非必要的括号展开并进行化简,得到只有必要括号的表达式。
分析字符串结构可以采用递归下降的方法,从设定的形式化表述出发,分别建立处理表达式、项、因子、变量因子、常数因子、表达式因子的操作。
以处理表达式为例,表达式的表述为:
表达式 \(\to\) 空白项 [加减 空白项] 项 空白项 | 表达式 加减 空白项 项 空白项
前一种情况很容易处理,考虑第二种情况,我们要找出“表达式”、“加减”、“项”三部分,因此我们可以找出倒数第二项后的第一个加减号将其作为划分(因为可能存在多个加减号),两部分分别按表达式和项各自处理。其他的情况也都可以类似处理,完全从形式化表述出发,形成一个天然的递归结构。这一做法实质是找到整个表达式中优先级最低的运算符,将左右两边作为表达式递归运算,构造了一个类似表达式树的结构,但是在实际操作中并不需要真的建树,只要每个表达式返回一个已化简的结果即可。
展开括号也可以采用类似的形式,分别建立表达式、单项式、幂函数、三角函数的元素类,每个类均返回处理完成的表达式结果,定义表达式的加减乘操作即可完成括号的展开。
如下为我的UML类图(简化了一些只在类内使用的函数):
架构分析
优点
- 思路清晰:本架构完全从形式化表述出发,在进行程序设计时,不需要设计特殊的操作(如设计形式化表述对应的正则表达式),不需要纠结是否有特殊情况没有考虑。在实际编写中,大多数情况下只要按照要求制作即可,一方面减少了编写难度,另一方面减少了错误的可能。
- 鲁棒性强:我们可以看到,在本题目中(尤其是前两个题目)为了降低难度而增加了诸多限制,如不存在嵌套括号等,但是利用这种架构,只要输入的表达式符合形式化表述,那么他就可以被解析。在本作业中,我从第二次作业到第三次作业只进行了极小的修改,按照第三次作业的要求对输出进行了一些特殊处理,原本的处理结构没有任何变化,这也足以体现这一架构的通用性。
缺点
结构破碎,这一点也可以从UML中看到,在编写代码过程中对代码结构的合理性有点忽略,下次作业应进行改正。
各个类的分析
所有的类可以大致分为两类,一类是用于分析表达式的类(Exponent,
Factor,
SelfDefinedFunction,
SumFunction,
Term,
VariableFactor
),一类是用于求解表达式的类(ExponentElement,
Monomial,
Function
),Expr,TriFunctionElement
在设计时考虑欠妥,同时具备这两个类的功能。
用于分析表达式的类都有一个构造函数,传入一个字符串,讲解析的结果Expr
保存起来,再通过一个方法getRes
返回结果;用于求解表达式的类都有一个方法toString
,用于返回最终得到的字符串,同时大部分也都有simplify
方法来完成表达式的化简。
基于度量的分析
Class | OCavg | OCmax | WMC |
---|---|---|---|
code.Exponent | 1.5 | 2 | 3 |
code.ExponentElement | 1.4 | 2 | 7 |
code.Expr | 3.09 | 18 | 68 |
code.Factor | 2 | 3 | 4 |
code.Function | 2 | 3 | 6 |
code.Hw2 | 1.5 | 2 | 6 |
code.Monomial | 3.06 | 13 | 49 |
code.SelfDefinedFunction | 2.75 | 5 | 11 |
code.SumFunction | 2.67 | 5 | 8 |
code.Term | 3.33 | 5 | 10 |
code.TriFunctionElement | 1.2 | 3 | 12 |
code.VariableFactor | 1.6 | 4 | 8 |
OCavg:类平均圈复杂度
WMC:类总圈复杂度
可以看到,复杂度主要集中在Expr和Monomial两个类中,具体原因为这两个类都涉及加减乘等运算、化简操作和代入操作,从而复杂度较高。对于WMC最高的Expr,个人分析这一部分有点设计失误,我将对表达式的分析和化简全部放到了这个类中,实际上应当将这两部分分开,有助于使设计结构更清晰,减小错误发生的可能。
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
code.Exponent.Exponent(String) | 1 | 2 | 1 | 2 |
code.Exponent.getPow() | 0 | 1 | 1 | 1 |
code.ExponentElement.ExponentElement(String) | 2 | 1 | 2 | 2 |
code.ExponentElement.ExponentElement(String, int) | 0 | 1 | 1 | 1 |
code.ExponentElement.getPow() | 0 | 1 | 1 | 1 |
code.ExponentElement.getVariable() | 0 | 1 | 1 | 1 |
code.ExponentElement.toString() | 2 | 2 | 1 | 2 |
code.Expr.Expr() | 0 | 1 | 1 | 1 |
code.Expr.Expr(BigInteger) | 0 | 1 | 1 | 1 |
code.Expr.Expr(Monomial) | 0 | 1 | 1 | 1 |
code.Expr.Expr(String) | 9 | 1 | 5 | 5 |
code.Expr.add(Expr) | 0 | 1 | 1 | 1 |
code.Expr.addMonomial(Monomial) | 0 | 1 | 1 | 1 |
code.Expr.findLastAddSubOperator(String) | 17 | 7 | 6 | 9 |
code.Expr.isFactor() | 1 | 2 | 1 | 2 |
code.Expr.merge() | 7 | 4 | 4 | 4 |
code.Expr.merge(Monomial, Monomial) | 41 | 14 | 8 | 18 |
code.Expr.monomialSize() | 0 | 1 | 1 | 1 |
code.Expr.mul(Expr) | 3 | 1 | 3 | 3 |
code.Expr.neg() | 1 | 1 | 2 | 2 |
code.Expr.pow(int) | 1 | 1 | 2 | 2 |
code.Expr.selfAdd(Expr) | 0 | 1 | 1 | 1 |
code.Expr.selfAdd(Monomial) | 0 | 1 | 1 | 1 |
code.Expr.selfMul(Expr) | 0 | 1 | 1 | 1 |
code.Expr.simplify() | 8 | 2 | 5 | 6 |
code.Expr.sub(Expr) | 1 | 1 | 2 | 2 |
code.Expr.substitute(String, BigInteger) | 1 | 1 | 2 | 2 |
code.Expr.substitute(String, Expr) | 1 | 1 | 2 | 2 |
code.Expr.toString() | 5 | 2 | 4 | 5 |
code.Factor.Factor(String) | 3 | 1 | 3 | 3 |
code.Factor.getRes() | 0 | 1 | 1 | 1 |
code.Function.Function(String) | 2 | 2 | 2 | 3 |
code.Function.getName() | 0 | 1 | 1 | 1 |
code.Function.substitute(ArrayList |
3 | 2 | 3 | 4 |
code.Hw2.exprInputRead() | 1 | 1 | 2 | 2 |
code.Hw2.getSelfDefinedFunctions() | 0 | 1 | 1 | 1 |
code.Hw2.main(String[]) | 0 | 1 | 1 | 1 |
code.Hw2.terminalRead() | 1 | 1 | 2 | 2 |
code.Monomial.Monomial(BigInteger) | 0 | 1 | 1 | 1 |
code.Monomial.Monomial(BigInteger, ArrayList |
0 | 1 | 1 | 1 |
code.Monomial.Monomial(Element) | 0 | 1 | 1 | 1 |
code.Monomial.getCoef() | 0 | 1 | 1 | 1 |
code.Monomial.getElements() | 0 | 1 | 1 | 1 |
code.Monomial.identity() | 1 | 1 | 2 | 2 |
code.Monomial.isFactor() | 2 | 1 | 3 | 3 |
code.Monomial.mul(Monomial) | 0 | 1 | 1 | 1 |
code.Monomial.neg() | 0 | 1 | 1 | 1 |
code.Monomial.oneElement() | 10 | 1 | 8 | 8 |
code.Monomial.selfNeg() | 0 | 1 | 1 | 1 |
code.Monomial.setCoef(BigInteger) | 0 | 1 | 1 | 1 |
code.Monomial.simplify() | 26 | 7 | 11 | 13 |
code.Monomial.substitute(String, BigInteger) | 8 | 1 | 5 | 5 |
code.Monomial.substitute(String, Expr) | 8 | 1 | 5 | 5 |
code.Monomial.toString() | 13 | 1 | 8 | 8 |
code.SelfDefinedFunction.SelfDefinedFunction(String) | 2 | 2 | 2 | 3 |
code.SelfDefinedFunction.findNextSplitter(int, String) | 6 | 5 | 5 | 6 |
code.SelfDefinedFunction.getParameters(String, int) | 1 | 1 | 2 | 2 |
code.SelfDefinedFunction.getRes() | 0 | 1 | 1 | 1 |
code.SumFunction.SumFunction(String) | 1 | 1 | 2 | 2 |
code.SumFunction.findNextSplitter(int, String) | 6 | 5 | 5 | 6 |
code.SumFunction.getRes() | 0 | 1 | 1 | 1 |
code.Term.Term(String) | 6 | 1 | 4 | 4 |
code.Term.findLastMul(String) | 7 | 3 | 6 | 8 |
code.Term.getRes() | 0 | 1 | 1 | 1 |
code.TriFunctionElement.TriFunctionElement(String) | 0 | 1 | 1 | 1 |
code.TriFunctionElement.TriFunctionElement(String, Expr, int) | 0 | 1 | 1 | 1 |
code.TriFunctionElement.TriFunctionElement(TriFunctionElement, Expr) | 0 | 1 | 1 | 1 |
code.TriFunctionElement.getContent() | 0 | 1 | 1 | 1 |
code.TriFunctionElement.getPow() | 0 | 1 | 1 | 1 |
code.TriFunctionElement.getTriFunction() | 0 | 1 | 1 | 1 |
code.TriFunctionElement.identity() | 0 | 1 | 1 | 1 |
code.TriFunctionElement.substitute(String, BigInteger) | 0 | 1 | 1 | 1 |
code.TriFunctionElement.substitute(String, Expr) | 0 | 1 | 1 | 1 |
code.TriFunctionElement.toString() | 3 | 1 | 3 | 3 |
code.VariableFactor.VariableFactor(String) | 4 | 1 | 4 | 4 |
code.VariableFactor.getRes() | 0 | 1 | 1 | 1 |
code.VariableFactor.isExponent(String) | 1 | 1 | 4 | 4 |
code.VariableFactor.isSelfDefinedFunction(String) | 1 | 1 | 3 | 3 |
code.VariableFactor.isTrigonometricFunction(String) | 1 | 1 | 2 | 2 |
v(G):圈复杂度
ev(G):非抽象方法的基本复杂度,描述了类内部自身的耦合性。
iv(G):设计复杂度,度量方法控制流与其他方法之间的耦合程度。
可以看到,复杂度主要集中在simplify相关操作,因为在对三角函数等进行化简时,需要考虑的细节较多,因此复杂度较高也不难预料,如果有人有复杂度低的实现方式欢迎交流。
除此之外,findNextSplitter
等类内方法的ev(G)较高,这提示这些方法整体上难以被分成若干个部分分别测试,必须整体来理解,从而增加了理解和测试的难度,这一分析也是合理的,在编写代码时大部分需要考虑的部分也来自于此,以后应注重减少这类操作,通过别的更易测试的方式来完成类似任务。
细节实现
1、指数的处理
新建类Exponent
来通过字符串得到指数。
package code;
public class Exponent {
private final int pow;
Exponent(String s) {
if (s.isEmpty()) {
this.pow = 1;
return;
}
this.pow = Integer.parseInt(s.substring(2));
}
int getPow() {
return this.pow;
}
}
2、找到正确划分的+-号
首先该+-号必须在括号外,而且他前面必须是项而不能是表达式,最终代码如下:
int findLastAddSubOperator(String s) {
int inBracket = 0;
for (int i = s.length() - 1;i >= 0;--i) {
char ch = s.charAt(i);
if (ch == ')') {
++inBracket;
}
else if (ch == '(') {
--inBracket;
}
else {
if (inBracket != 0) {
continue;
}
if (ch == '+' || ch == '-') {
if (i != 0 && !"+-*".contains(Character.toString(s.charAt(i - 1)))) {
return i;
}
}
}
}
return -1;
}
3、合并同类项
单项式和多项式合并同类项的方式类似,区别只在于一个是加法一个是乘法,
在此以多项式(Expr)为例:
建立两个TreeMap,均以不包括系数的单项式字符串为关键字,保存该单项式的系数和和单项式本身,最后遍历TreeMap取出所有的单项式即完成了同类项的合并。
void simplify() {
if (this.monomials.size() <= 1) { return; }
TreeMap<String,BigInteger> coefHashMap = new TreeMap<>();
HashMap<String,Monomial> monomialHashMap = new HashMap<>();
for (Monomial monomial : this.monomials) {
monomial.simplify();
if (coefHashMap.containsKey(monomial.identity())) {
BigInteger coefInHashMap = coefHashMap.get(monomial.identity());
coefHashMap.replace(monomial.identity(),coefInHashMap.add(monomial.getCoef()));
}
else {
coefHashMap.put(monomial.identity(),monomial.getCoef());
monomialHashMap.put(monomial.identity(),monomial);
}
}
ArrayList<Monomial> res = new ArrayList<>();
for (String identity : coefHashMap.keySet()) {
if (!coefHashMap.get(identity).equals(BigInteger.ZERO)) {
res.add(new Monomial(coefHashMap.get(identity),
monomialHashMap.get(identity).getElements()));
}
}
this.monomials.clear();
this.monomials.addAll(res);
}
4、自定义函数的代入
定义substitute
函数,以ArrayList<String> variables,ArrayList<BigInteger/Expr> values
为参数,相当于是将表达式树上的某个点替换为该BigInteger/Expr
。
随机数据生成器
我使用了基于python的subprocess和sympy来进行数据生成和对拍,思路大致为通过BNF描述来递归的构造数据,同时通过传入参数来控制递归的深度和表达式/项的长度。
def generate_expr(max_level,length,funcs,variables):
if length == 1:
s,sym = generate_term(max_level,2,funcs,variables)
ret = add_plus_sub(s,sym)
else:
expr_str,expr_sym = generate_expr(max_level,length - 1,funcs,variables)
term_str,term_sym = generate_term(max_level,2,funcs,variables)
if randint(1,2) == 1:
ret = expr_str + "+" + term_str,expr_sym + term_sym
else:
ret = expr_str + "-" + term_str,expr_sym - term_sym
return ret
其他均类似,不再赘述。
对于自定义函数,可以通过将变量传入的方式同时传入x、y、z三个变量,在生成数据时任选一个进行生成。
def generate_func():
return generate_expr(max_level=2,length=2,funcs=[],variables=[Symbol('x'),Symbol('y'),Symbol('z')])
最后,使用sympy的simplify和equals比较即可。
发现的bug
1、由于Hashmap的keySet未排序导致的同类项没有正确合并。
问题描述
HashMap的keySet未对其元素进行排序,因此可能本质相同的两个式子未被简化成同一形式,导致之后未能被合并。
解决方法
将HashMap改为TreeMap,虽然复杂度多了一个log但其内部元素是排好序的。
2、0**0的问题
问题描述
一是没有正确处理0**0
的形式,特别是sin(0)**0
的形式。二是没有对这种零次幂进行简化。
解决方法
对这些形式进行特殊处理。
3、无用简化导致的超时问题
问题描述
本人为了防止出现应该合并的同类项没有合并的问题,再很多地方调用了simplify操作,而simplify中合并同类项也需要每一个单项式的simplify,这导致了当括号嵌套层数过多时simplify操作达到了指数级从而造成超时。
解决方法
删除一些无用的simplify,对只有一个单项的多项式进行特殊处理从而避免simplify的指数级递归调用。
4、函数参数的重复代入问题
问题描述
在代入时采用了一个参数一个参数代入的情况,因此后面的参数可能会带入之前已经代入的结果。
解决方法
将参数按ArrayList传入,同时进行所有替换。
在编写这一代码时,我先编写了将某个变量进行替换的方法,在调用时直接用一个循环调用三次,这一“偷懒”做法也最终导致了这一bug,在测试时,我也想到这里可能会出问题,但并未深入思考和测试,以后应避免类似情况的出现。
bug分析
可以看到,这四个错误虽然并没有都在我这里出现,但是它们都出现在了复杂度最高的simplify上,这对我以后的代码编写和测试也是一个警示。
测试及hack策略
一是利用自动评测系统,不断用大量随机数据进行测试,优点是理论上可以测出任何bug,更适合测试不同的组合,缺点是实际上测出bug的概率很小,而且如果在生成数据时没有生成某个特殊情况,那么在该特殊情况发生的bug自然无法被测出,比如说我在生成自定义函数时按x,y,z的顺序生成形参,上文代入的bug也就不能被测出。
二是手动构造数据,这包括构造边界数据和各种可能性的数据,可以在hack前就预先准备一些特殊的数据,同时针对x**+01等类似的特殊情况进行测试。
心得体会
通过本次OO开发,我对JAVA语言的了解更加深入,对于代码质量的感受更加透彻,也对软件测试有了一点初步的了解,也认识到将复杂问题一步步拆解从而降低复杂度,提升编码效率的思想。
但是本次作业也有些许遗憾,最大的遗憾在于:本次作业里我很少使用我认为非常纯粹的面向对象思想。本次作业表达式预处理部分我认为在我的架构中大部分没有必要分成类,写一个读入String
返回Expr
的函数在代码上更加简洁,Expr
和Monomial
中主要函数有两部分,一是加减乘等运算;二是simplify
与substitute
,这一部分可能是我认为这次作业唯一一次体现“对象”、“行为”等概念的地方了,这样的结果就是我最后的UML类图如上所示几乎没有什么结构可言。在这方面有独到理解的欢迎和我多探讨。
最后,感谢各位助教的付出,感谢和我交流的同学,期待之后的OO课程。