OO_summary_Expression
前言
OO第一单元的主题是表达式括号展开,将带括号的符合跟定文法的表达式修改为不带括号的,依然符合给定文法的表达式。括号展开的过程完全可以使用面向过程的思想,利用后缀表达式和复杂的正则表达式来完成。但我们训练的重点在于面向对象的思维(合理设计类并体会封装,继承,多态在面向对象程序设计种带来的好处。第一单元处理多项式,第二单元加入了三角函数,第三单元加入了求导因子,三次作业层层递进,具有较强的迭代性。
处理括号只是正确性的必然要求,要将表达式展开的简洁,又是一个大的方面,于是就出现了大肆优化卷性能分以及强测全过喜提89分的冥场面…
hw1分析
第一次作业主要是关于多项式的展开,并没有其他复杂的函数。文法如下:
疑点分析
在一开始写Parser的时候,总是不确定分析到某步依据什么信息来选择相应的分析模块(标识符)。后来我将
表达式 → 空白项 [加减 空白项] 项 空白项 | 表达式 加减 空白项 项 空白项
大胆展开发现表达式=[加减]项 加减 项 加减 项 … 加减项,顿感醍醐灌顶,找到了分析的思路,只需要 a n a I t e m anaItem anaItem之后判断有没有加减号即可!
简要思路
因子及表达式存储形式
不管你的思路多么花里胡哨,最终总是逃不开一个问题,你所谓的“表达式”“因子”,怎么去存储所有的信息! 换言之,选取怎么样的数据结构。在实验课上我见到了极其完美的架构(可惜已经记不清了),这里说一下我的朴素版架构。
最终表达式无非是化简成
i
t
e
m
1
+
i
t
e
m
2
+
.
.
.
+
i
t
e
m
n
item_1+item_2+...+item_n
item1+item2+...+itemn的形式,
i
t
e
m
item
item要么是常数因子,要么是幂因子,可以统一看成
c
o
e
∗
v
a
r
1
p
o
w
1
∗
v
a
r
2
p
o
w
2
∗
v
a
r
3
p
o
w
3
coe*var_1^{pow_1} *var_2^{pow_2}*var_3^{pow_3}
coe∗var1pow1∗var2pow2∗var3pow3的形式,视为PowerFactor。至于常数因子,其实也就是BigInteger,不过为了使得各种因子能统一操作,我建立了ConstFactor类和Factor接口,并将各种Factor实现Factor接口。这样Expr类就可以用ArrayList<Factor> facArr来表示。
public class ConstFactor implements Factor{
private BigInteger val;
}
public class PowerFactor implements Factor{
private BigInteger coe;
private ArrayList<String> nameArr;
private ArrayList<BigInteger> expArr;
}
public class Expr {
private ArrayList<Factor> facArr;
}
这便是我们的存储结构,这里我没有将表达式看成一个因子,因为我将因子理解为用"+"和”-“连接起来的最简形式(但其实将Expr看成因子也有其合理性,比如表达式也可以和因子相乘,更加贴合文法等)
因子及表达式的行为
这个无非是幂次,乘法,加法操作,面向过程的性质较强,没什么好说的。
Parser类
Parser类是我们进行递归下降的非常必要的一个类,但同时也是最好设计的一个类,你只要对文法中涉及到的每一个东西写一个analyze方法,就可以顺着文法来对文本进行分析,我这里采用的是分析一个就把他化到最简的形式,由于化简后极有可能是Expr,所以返回时皆是Expr。
public Expr anaExpr();
public Expr anaItem();
public Expr anaPowerFactor();
public Expr anaConstFactor();
....
表达式及因子的深浅拷贝问题
- 这里运算的时候一般就是深拷贝,谁也不想自己改变一项另一项也因此变化。
- 想达到深拷贝的效果,不必每一个对象都new一下。BigInteger是一个不可变对象(其add,sub等方法只会生成一个新的对象,而不会对已有的对象进行更改),浅拷贝即可达到深拷贝的效果!
- 特别注意ArrayList的深拷贝问题!!!重要的事情说三遍!
笔者就犯了一个很严重的深浅拷贝错误,但是在之前的作业中触发不了,直到第三次作业由于优化函数才触发了这个bug。
public Expr clone(){ Expr expr=super.clone(); for(int i=0;i<facArr.size();i++){ expr.facArr.set(i,facArr.get(i).clone()); } return expr; }
表面上似乎expr的factor数组的每一个对象都是原来的深拷贝副本,但是Java的对象标签本质上是类似C语言的指针的东西,而且Object的clone方法只是浅克隆,相当于expr和this的facArr都是同一个ArrayLIst的引用,操作了expr的facArr,this的facArr也会变,这个clone方法最终与**return (Expr) super.clone()**没有什么差别,所以这个依然只是一个浅拷贝!
输出策略
为每一个因子写出一个toString方法,Expr对象只需将每一个factor.toString() 拼接得到一个字符串(连接因子的正负号由因子本身的正负决定,在因子自身的toString方法中处理)。
这里提一下笔者的一个错误,我想要将facArr中的0删除,是否删除由deletable方法决定。
\\ConstFactor's deletable method
public boolean deletable(){
return val.equals(0);
}
\\PowerFactor's deletable method
public boolean deletable(){
return coe.equals(0);
}
可以看到我的目的是判断一个BigInteger对象是不是0,但以上做法本质上不对,因为val是一个对象,BigInteger的equals方法用Object接受任何一个对象,这里0传进去被自动装箱成了Integer对象,由于不是BigInteger类,直接被返回了false,导致0不能成功删除。
至于系数为"+1"和”-1“,以及幂次为”1“的简化输出,逻辑简单,不再赘述。
同类项合并
主要是对 P o w e r F a c t o r PowerFactor PowerFactor 中facArr中的因子进行合并,根据上述架构,可以得到合并的原则。
- ConstFactor同类皆可合并
- PowerFactor若nameArr.size()==0,即只有一个系数的情况,可以降阶为ConstFactor,与其他ConstFactor合并
- 两个PowerFactor合并,要求所含自变量相同且自变量对应的幂次也应该相等,合并时将系数相加即可。
hw2分析
hw2相比于hw1只是加入了三角函数以及自定义函数。
三角函数的处理
三角函数加入一个类就可以解决,加入三角函数后,”项“的最基本形式变成了 P o w e r F a c t o r ∗ s i n ( e x p r 1 ) p o w 1 ∗ s i n ( e x p r 2 ) p o w 2 . . . . PowerFactor*sin(expr_1)^{pow1}*sin(expr_2)^{pow2}.... PowerFactor∗sin(expr1)pow1∗sin(expr2)pow2....的形式,我想利用先前写好的PowerFactor来组建新的项 I t e m F a c t o r ItemFactor ItemFactor,于是将三角函数设计的非常的局限,其为 s i n ( e x p r ) p o w sin(expr)^{pow} sin(expr)pow的形式,不需要系数。然后ItemFactor就是PowerFactor加上一个三角函数为元素的ArrayList即可。
public class TriFactor implements Factor, Cloneable {
private boolean sinOrcos;
private BigInteger exp = BigInteger.ONE;
private Expr expr;
}
public class ItemFactor implements Factor, Cloneable {
private PowerFactor powerFactor = new PowerFactor();
private ArrayList<TriFactor> triArr = new ArrayList<>();
}
这的确可以完全的反应出信息来,但明显三角函数,幂函数,与项因子之间处于不同的层次关系,这实际上大大阻碍了将因子看成整体的思想,在后面写优化的时候造成了极大的困扰。
自定义函数处理
由于hw3中的自定义函数可以调用其之前定义的自定义函数,我这里直接对一般的情况进行处理。
思路:
- 由于同一个expr在引入了不同的自定义函数后调用Parser会得到不同的化简结果,我这里采用
HashMap<String,Function>funcSet
作为parser的一个属性。 - Functiuon类存储了不含括号与自定义函数的函数表达式字符串,以及参数数量,参数名称等信息,主体如下:
public class Function{
private int argNum;//参数数量
private String[] argName;//参数名称
private String expr;//不含自定义函数的表达式
private String funcName;//函数名
public Expr eval(Expr... exprs) throws CloneNotSupportedException {
String temp = expr;
for (int i = 0; i < argNum; i++) {
String exprFac = "(" + exprs[i].toString() + ")";
temp = temp.replaceAll(argName[i], exprFac);
}
Parser parser = new Parser(temp);
return parser.anaExpr();
} //将exprs[i]以文本的形式类似宏作替换
}
其主要方法eval先将传入的表达式以文本的形式替换对应的形参名,得到一个最终的字符串,再利用Parser对这个字符串进行解析,返回Expr形式存储的表达式。
注意
由于替换是有先后顺序的,并不能做到同时!,对于形参与实参出现有相同字符的情况时,就会出现错误!举个例子:
定义
f
(
x
,
y
)
=
x
+
y
f(x,y)=x+y
f(x,y)=x+y,求
f
(
y
,
x
)
f(y,x)
f(y,x),显然答案应该是
x
+
y
x+y
x+y.
但若我们先将表达式里的‘x’全替换为’y’,就会得到 y+y,然后再将表达式里的y全替换为x,最终得到2*x,显然错误。
- 解决办法
- 预处理
我们在定义时将表达式处理为字符串时,得到的是f=L(x,y,z)的形式(注意,甚至形参的顺序都不一定是x,y,z先后出现),但必定有第一个出现和最后一个出现的,我们永远将第一个出现的标记为‘p’,第二个出现的’q’,以此类推这样就可以保证第一个参数永远替换第一个形参。我们在得到f(var1,var2,var3)=str(var1,var2,var3)时,利用replaceAll方法,将其替换为字符串str(p,q,r).这样再看上面的例子:- 首先将p+q中的p替换为y,得到y+q,接着再将q替换为x,得到x+y。结果正确
这本质上利用了形参与实参字母不重复的原理
note
这里其实还有一个细节,我们选用了p,q,r而不是a,b,c,这就是笔者曾犯的一个错误。原因在于cos中含有字母’c’,导致将表达式中的c完全替换为x后,得到xos这样不伦不类的东西 - 首先将p+q中的p替换为y,得到y+q,接着再将q替换为x,得到x+y。结果正确
- eval调用
传入的exprs是一个Expr[]数组,但我们已经为表达式写好了toString方法,所以可以直接在字符串层面上进行操作。重要但容易想到的一点,替换时不要忘了加括号:‘’(‘’+exprs[i]+‘’)‘’ - 预处理时遇到自定义函数调用的问题.
这是一个必须考虑的问题,例如定义时 g(x,y)=f(x,y)+z的情况。我的做法是,既然我们已经有了f(x,y)的Function对象(调用的都是定义过的),虽然我们的Parser的funcSet并不完整,但已经可以用来解析设计到的自定义函数了,也就是继续用parser解析我们的定义式。
我们利用Parser.anaExpr方法将定义式子解析后,可以得到**正确的str(x,y,z)**的形式,再次依据出现先后次序将参数名替换为p,q,r。就能生成Function对象。
note
这也是笔者犯的一个错误,对于有定义时调用自定义函数的情况,你要注意先正确的解析后,再替换变量名。我之前考虑不周,直接先将代替换的exprs里的x,y,z替换为p,q,r后再替换,这样就会出现之前的形参与实参有字母重合的情况,是一个很严重的错误!
Parser处理
直接在之前parser的基础上根据新加入的文法添加anaTriFactor方法即可,parser的迭代处理相当简单.之后的parser也不过如此,不再赘述。
hw3中求导因子的处理
求导是因子的行为,应当为每个因子设置求导方法,由于因子求导之后可能变成表达式,所以我统一返回值类型为Expr,并在Factor接口中定义此方法。
Expr diffBy(String varName) throws CloneNotSupportedException;
各个Factor类型对象皆需要实现此方法,笔者在实现求导方法时,发现了架构的严重不足,就在于各个因子的不平等地位。可以看到我的架构时层次话的,ItemFactor实际上地位高于TriFactor等因子,求导时就写的很难受。最终也是写出了bug导致强测错了3个测试点。架构很重要!!!!
化简方法
由于笔者采用的存储方法,导致化简过程及其复杂…都是泪啊
化简必须考虑的几个问题
:
- sin(0)与cos(0)的化简问题
- sin(x)与sin(-x)如何将其合并
- sin(x)与sin(y) 如何判定合并
- sin(expr)如何判断expr是一个因子以减少括号
- f ( x ) ∗ s i n ( x ) 2 f(x)*sin(x)^2 f(x)∗sin(x)2与 g ( x ) ∗ c o s ( x ) 2 g(x)*cos(x)^2 g(x)∗cos(x)2的相加相减如何判断可以合并,怎么合并
-
s
i
n
(
e
x
p
r
1
)
p
o
w
1
∗
s
i
n
(
−
e
x
p
r
1
)
p
o
w
2
.
.
.
.
.
sin(expr1)^{pow1}*sin(-expr1)^{pow2}.....
sin(expr1)pow1∗sin(−expr1)pow2.....如何将其中互为相反数的表达式识别出来,合并幂次
当然还有人化简二倍角公式等(导致笔者性能分喜提80),笔者只考虑了这些,并没有化简二倍角公式。
sin(expr)的因子判断问题
- expr经过simplify函数化简后,其facArr只能最多有一个元素,否则不是因子
- 若为TriFactor或者ConstFactor,必定为因子
- 若为PowerFactor,只有这几种可能
- 1*x的形式
- number的形式
- 若为ItemFactor,只有这几种可能
- powerFactor必须是因子,否则不是
- powerFactor是因子,且triArr.size()==0
- triArr.size()1且powerFactor.toString()‘+1’
笔者在写这东西时也是费了九牛二虎之力…
必须实现的equals方法
由于我的Expr采取的是乱序的ArrayList结构,因此判断expr1和expr2的方法就是
- expr1.facArr.size()==expr2.facArr.size()
- for factor in expr1.facArr :
expr2.facArr.contains(factor)
这里需要注意,ArrayList容器的contains方法需要用到equals方法,你需要重写此类的equals方法,说一个笔者犯的错误.
note
笔者在Factor接口中定义了boolean equals(Factor e)
,然后将各个因子类实现了此方法,但是却没有重写Object类的equals方法,而ArrayList所用到的equals方法不是我在接口中的equals方法,而是Object类的equals方法(两者参数类型不同),而Object类的equals方法只是比较两者是否是同一个对象的引用,就导致我的equals方法无效!
一定要重写 public boolean equals(Object e)方法
判断是否相反可以用equals(expr.negate())来代替
sin(-x)的问题
如果发现某两个三角函数的expr相反,只有当其为sin函数,且幂次为奇数时,才需要变化符号!
其他情况笔者的架构写起来非常复杂,处理平方和的函数写了整整300+行,与checkstyle缠斗俩小时才搞定代码风格的问题,在这里就不详细说了,正确性以及基本的优化以上内容已经说清楚了
代码架构分析
代码复杂度分析
先来介绍一下几个指标:
- 方法衡量指标
指标 | 含义 |
---|---|
CogC(Cognitive complexity) 认知复杂度 | 衡量一个方法的控制流程有多困难去理解。具有高认知复杂度的方法将难以维护。sonar要求复杂度要在15以下。计算的大致思路是统计方法中控制流程语句的个数 |
ev(G)(essential cyclomatic complexity) 方法的基本圈复杂度 | 衡量程序非结构化程度的 |
iv(G) (Design complexity) :设计复杂度 | 字面意思 |
v(G)(cyclomatic complexity) :方法的圈复杂度 | 衡量判断模块的复杂度。数值越高说明独立路径越多,测试完备的难度越大 |
- 类的衡量指标
指标 | 含义 |
---|---|
OCavg(Average opearation complexity) | 平均操作复杂度 |
OCmax(Maximum operation complexity) | 最大操作复杂度 |
WMC(Weighted method complexity) | 加权方法复杂度 |
利用IDEA的MetricsReloaded插件分析,得到Method Metrics,由于太长,只选取一些具有超标性质的方法。
经过实践,这个表给我的体验就是有IDEA标红属性的方法,都是一些面向过程性特别强,特别基础而又有相对复杂逻辑的函数;这真的不是空穴来风,事实证明我出现的bug确实就是在这几个函数中。
越复杂逻辑的代码出错的概率越高,越需要我们着重检查与测试!Metrics工具今后将是我们白盒测试的重要依据!
程序Bug
我自己的很多Bug都在上面提到了,但其实强测测不出来我的很多bug,有个深浅拷贝的bug第一次作业就有,知道第三次作业才因为优化而触发。
强测测出来的Bug都是自己写代码的过程中大脑突然断了一下神导致的低级而又严重的错误,所以一定要细心多做测试。
互测中我一般采用使用评测机狂轰乱炸的方式对同学的代码进行地毯式的扫描。在经过de自己的bug之后,其实也可以通过直接看同学的代码找出一些明显的深浅拷贝bug。说到底还是经验加评测机相结合的方式。
心得体会
- 递归下降的思想是简单而有用的,在之后的学习中要多多使用
- 千万别对自己的代码有太多的自信,一定要测试测试测试!
- 要注重构思,三次作业说来惭愧,最后都是好像写完了又不知道写到哪了,bug在所难免
- ArrayList类型的深拷贝千万注意