北航OO第一单元作业总结
OO_summary_Expression
前言
OO第一单元主要要求将一个表达式展开、化简,同时要求化简结果正确而且不能带有不必要的括号。这次作业是我第一次接触java这类面向对象的编程语言,因此在完成作业时较为吃力,而且初次完成设计时有很多不健全的架构设计,因此经过了一次重构。
本单元一共又三次作业,三次作业层层递进,这对代码的可拓展性有一定要求,而多种因子类型又要求实现多态,这让我体会到面向对象思维的重要性,加深我了对封装,继承与多态等性质的理解,体会到了不要重复造轮子的必要性。
1. Homework1
第一次作业要求不高,只有加减乘三种运算,最多一层括号,因子只有四种(在我的程序中只有两种,我将指数整合到了每一个因子中)。输出要求不含括号,而且字符串要尽可能的短。
1.1 UML类图
1.2 难点分析
第一次作业的难点主要在于对java语法的认识,对面向对象思想的理解,已经对递归下降的理解。虽然要求算不上高,但是难度还是相对较高的,让我体验到了艰辛的一周。
在我看来,这一章的实现主要的难点在于解析表达式,也就是 Parser
类的实现,要考虑每一种情况,分析每一个数字所在的位置,如果解析表达式出错,结果会出现错得很离谱的情况。要找出问题所在,调试不可或缺。
1.3 实现过程。
本程序实现的过程是先解析解析表达式,构建一颗树,然后遍历整棵树,在这个过程中化简,返回一个数组。遍历结束后,将返回的数组进行化简,最后输出。
1.3.1 输入预处理
输入的表达式可以通过一些预处理来简化后面的解析过程。
- 将空格已经换页符去掉,这里可以利用replaceAll方法。
- 将连续的
+
和-
简化为一个+
。 - 将相邻的
+
与-
简化为一个-
。
input = input.replaceAll("\\*\\*", "^");
input = input.replaceAll(" ", "");
input = input.replaceAll("\t", "");
input = input.replaceAll("\\+\\+", "+");
input = input.replaceAll("--", "+");
input = input.replaceAll("\\+-", "-");
input = input.replaceAll("-\\+", "-");
1.3.2 解析表达式
解析表达式主要利用 Lexer
与 Parser
两个类。解析表达式主要通过递归下降的算法,来构建表达式的树。
Lexer
类通过next()
方法读取下一个数字或参数,通过peak()
方法返回目前读取的值。Parser
类负责解析表达式,并且生成所需的树。有以下三个方法。
public Expr parserExpr(int coefficient);
public Term parserTerm(int coefficient);
public Factor parseFactor();
顾名思义,分别是解析表达式,项,因子的方法。其中,对括号以及指数的处理最容易出错。
1.3.3 数据结构
存储的数据主要有三种类型, Expr
, Term
, Factor
类,其中 Factor
是一个接口,用于实现后续的多态。这三种类中都有着一些相同的数据类型。
private final String name;
private final int pow;
private final BigInteger coefficient;
第一个是 String
类型,可以是一个数字,可以是一个变量,也可以是一个表达式。第二个是 int
类型,代表该表达式的次数,通过这一方法,将指数因子表达出来。最后的 BigInteger
类型表示该表达式的系数,采用 BigInteger
为了预防输入的数值太大,超出long的范围。
-
Expr
类由Term
类聚合,数学关系如下。
E x p r = ∑ c o e f f i c i e n t ∗ n a m e p o w Expr = \sum coefficient*name^{pow} Expr=∑coefficient∗namepow -
Term
类由Factor
类聚合,数学关系如下。
T e r m = ∏ c o e f f i c i e n t ∗ n a m e p o w Term = \prod coefficient*name^{pow} Term=∏coefficient∗namepow -
Factor
是一个接口,表示因子,连接Expr
和Number
两个类。 -
Number
类是最为基础的数据类型,返回的数组也是Number
类的数组。由于其中的String
可以表示变量,数字,以及表达式,因此较为灵活,可以将所有的数据类型都转化为一个Number
的一个链表,后面的遍历表达式树也是基于这一点实现的。
1.3.4 遍历表达式树。
遍历表达式树同样通过递归的方式完成,主要通过 merge()
方法来实现。其效果是将该数据类型转化为一个等价的 Number
类的链表。
V
a
l
u
e
=
∑
c
o
e
f
f
i
c
i
e
n
t
∗
n
a
m
e
p
o
w
Value = \sum coefficient*name^{pow}
Value=∑coefficient∗namepow
其数值如上。该函数主要依赖之前生成的表达式树递归实现,根据 Expr
, Term
和 Factor
之间的数学关系(数学关系在上面数据结构部分)进行运算,直到递归到Number
类,返回一个仅含有本身的一个链表。 为了实现链表的加法和乘法,这里要写一个实现链表运算的方法类 MathMultionmial
来实现。
public static ArrayList<Number> multinomialAdd(ArrayList<Number> num1, ArrayList<Number> num2);
public static ArrayList<Number> multinomialMul(ArrayList<Number> num1, BigInteger coefficient);
public static ArrayList<Number> multinomialMul(ArrayList<Number> num1, ArrayList<Number> num2);
public static ArrayList<Number> multinomialPower(ArrayList<Number> num1, int power);
public static boolean isNumeric(String string);
multinomialAdd
方法用于实现链表相加。multinomialMul
方法用于实现链表间相乘或量表和数字相乘。multinomialPower
方法用于实现链表的乘方运算。isNumeric
方法用于判断一个Number
是不是数字,后面化简会用到。
通过这个方法,我们可以得到一个Value和原表达式相同的链表。
1.3.5 化简,合并同类项
在得到链表后,主要运用 Sort
方法对链表进行化简排序。
public static Number numberSort(Number number);
public static ArrayList<Number> multinomialSort(ArrayList<Number> list);
numberSort
方法会将数字全部识别,将其乘到系数中,也会将相同的未知数合并,这里调用了lexer
方法。如果最后的name为空,会变为1。multionmialSort
方法会查找name和power相同的number
进行合并,系数为0的number
会被抛弃。如果最后list为空的话,会返回一个含有0的list。
1.3.6 输出
输出也可以进行化简优化。
- 当系数为1时,可以省略输出系数。
- 当输出第一个数而且系数为正时,可以省略输出符号。
- 当系数为2,而且name只有一个字符时,输出
x*x
比x**2
字符要少。
1.4 遇到的bug
这次作业我在强测和互测中都没有发现bug,下面是我在自己线下检查时发现的bug,在互测时,我用我线下遇到的bug对别人进行测试。
1.4.1 Parser读取错误
当我输入形如 (1+x*y+y**2*x**2)**2
的数据时,由于我对于括号和指数的读取不够简洁,出现了读取错误,无法正确读取表达式因子的次数。
1.4.2 没有对空链表特判
当结果正好为0时,程序会什么也不输出,这里需要进行特判,当获得的list为空时,要输出一个name为"0"的 Number
列表。
1.5复杂度分析。
- 方法复杂度指标
指标 | 含义 |
---|---|
CogC | 此复杂度用来评判代码的阅读难点和测试难度。 |
ev(G) | 基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。 |
iv(G) | 模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。 |
v(G) | 是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。 |
- 类的复杂度指标
指标 | 含义 |
---|---|
OCavg | 平均操作复杂度 |
OCmax | 最大操作复杂度 |
WMC | 加权方法复杂度 |
方法太多,这里只列出超标的类和类的总复杂度以及均值。
- 方法复杂度
- 类复杂度
可以看到超标的方法主要为输出以及排序的类,主要为优化部分,其中存在大量的if-else分支以及多层for循环遍历,在编写代码时要避免多层for,if-else的嵌套,这样会极大的增加阅读难度以及debug难度。此外,Parser
类也有指标超标,同样是因为大量的if-else语句导致。此次为我的第一次作业,对面向对象的思想理解不够深刻,将输入输出部分直接写到MainClass
这导致输入输出方法无法多次调用,也使得主类变得臃肿难以维护,这导致了我后续的重构。
2. Homework2
本次作业新增了对多层括号嵌套解析的要求,同时加入了cos,sin,以及自定义函数的因子,复杂程度极大的提升;由于对java语法和面向对象过程的逐渐熟悉,本次作业并没有让我像第一次作业那样焦头烂额,但是也让我狠狠的爆肝。总的来说,难度相比于上次有所降低,但是依旧有不小的难度。
1.1 UML类图
可以看到新增了 Input
类以及Output
类,将主类进行了简化,同时增加 Cos
, Sin
,和Function
类三种新的类,都继承了 Factor
接口。
2.2 难点分析
第二次作业的难点主要在于新增加的自定义函数的处理,都要加入新的读取和处理方式。此外,就是 cos
和 sin
对于之前结构处理的冲击,cos内的表达式不能进行分割。此外,最大的难点就是三角函数的化简,不过我摆了。
2.3 实现过程
之前已经叙述过的部分这里就不过多赘述,仅仅讨论新增的部分和修改的部分。
2.3.1 自定义函数的读取与存储。
本次作业新增的自定义函数,需要重写输入函数进行读取。这里采用了while循环嵌套来分割输入的几个部分,利用好 ()
,=
等符号的位置。同时创建了一个静态 Fuc
类来储存读取的函数表达式。其结构如下。
public static class Fuc {
private final ArrayList<String> parameters;
private final String functionExpr;
private final String name;
public ArrayList<String> getParameters() {
return parameters;
}
public String cloneFunctionExpr() {
return String.valueOf(functionExpr);
}
public String getName() {
return name;
}
public Fuc(String name, String functionExpr, ArrayList<String> parameters) {
this.functionExpr = functionExpr;
this.parameters = parameters;
this.name = name;
}
}
- name用来存储函数名。
- functionExpr用来存储函数表达式。
- Parameters为一个链表,用来存储变量名。
- 在克隆函数表达式的时候,一定要使用
valueof()
方法进行深克隆,以防将原表达式修改。 - 我采用了简单粗暴的字符串替换,所以不能用重复的x,y,z变量,要将其替换,以防出现错误替换的情况。
2.3.3 Parser的修改
本次要实现对cos,sin以及自定义函数的读取。
-
可以通过对读取到字母后的一个字符进行判断,若字符是
(
则该字符为函数名,否则为变量名。 -
可以通过对字母进行
equal()
比较,如果是cos或者sin则建立一个Cos
或者Sin
,再调用factorParser
方法对函数内的因子进行读取。 -
Function
的读取,可以利用一个循环,将参数传入。public ArrayList<Factor> parserFunction() { ArrayList<Factor> factors = new ArrayList<>(); while (!lexer.peek().equals(")")) { lexer.next(); factors.add(parseFactor()); } lexer.next(); return factors; }
-
将读取指数的过程封装成一个方法,提高代码可读性,减少重复造轮子。
public int parserPower() {
if (lexer.peek().equals("^") || lexer.peek().equals("@")) {
lexer.next();
while (lexer.peek().equals("+")) {
lexer.next();
}
int n = Integer.parseInt(lexer.peek());
lexer.next();
return n;
}
return 1;
}
-
效果比对
-
修改前
int coefficient = 1; if (lexer.peek().equals("+")) { lexer.next(); } else if (lexer.peek().equals("-")) { coefficient = -1; lexer.next(); } if (lexer.peek().equals("(")) { lexer.next(); Expr expr = parserExpr(coefficient); lexer.next(); if (lexer.peek().equals("^")) { lexer.next(); while (lexer.peek().equals("+")) { lexer.next(); } expr.changePow(Integer.parseInt(lexer.peek())); lexer.next(); } return expr; } else { String name = lexer.peek(); lexer.next(); if (lexer.peek().equals("^")) { lexer.next(); while (lexer.peek().equals("+")) { lexer.next(); } int pow = Integer.parseInt(lexer.peek()); lexer.next(); return new Number(name, pow, BigInteger.valueOf(coefficient)); } return new Number(name,1, BigInteger.valueOf(coefficient)); } }
-
修改后
int coefficient = 1; if (lexer.peek().equals("+")) { lexer.next(); } else if (lexer.peek().equals("-")) { coefficient = -1; lexer.next(); } if (lexer.peek().equals("(")) { lexer.next(); Expr expr = parserExpr(coefficient); lexer.next(); expr.changePow(parserPower()); return expr; } else { String name = lexer.peek(); lexer.next(); return new Number(name, parserPower(), BigInteger.valueOf(coefficient)); }
-
2.3.4 三角函数
三角函数总的来说实现难度不大。其结构如下(Cos
与 Sin
类似,这里只展示 Cos
)。
public class Cos implements Factor {
private final Factor factor;
private final BigInteger coefficient;
private final int power;
@Override
public ArrayList<Number> merge() {
ArrayList<Number> list = factor.merge();
list = Sort.multinomialSort(list);
StringBuilder stringBuilder;
stringBuilder = new StringBuilder();
stringBuilder.append("sin(");
String expr = Output.getAnswer(list);
if (expr.equals("0")) {
ArrayList<Number> ans = new ArrayList<>();
ans.add(new Number("0",power,BigInteger.ONE));
return ans;
}
else if (expr.indexOf('*') == -1 && expr.indexOf('+') == -1 && expr.indexOf('-') == -1) {
stringBuilder.append(expr);
stringBuilder.append(")");
}
else {
stringBuilder.append("(");
stringBuilder.append(expr);
stringBuilder.append("))");
}
ArrayList<Number> ans = new ArrayList<>();
String string = stringBuilder.toString().replaceAll("\\^","@");
ans.add(new Number(string.replaceAll("\\*","%"), power, coefficient));
return ans;
}
public Cos(Factor factor, int power, BigInteger coefficient);
}
- 可以发现里面含有一个含有一个
Factor
类,可以调用Factor
类的merge()
方法来化简内部因子,再用Sort
类的方法进行化简排序。 merge()
方法可以直接返回一个cos的表达式,之前的程序可以直接处理。- 对内容为0的可以进行特判,直接返回0。这里要注意power,不能直接返回0,否则会出现
sin(0)**0 = 0
的错误。 - 括号内的表达式内不含
*
或者+
,-
时可以只输出一对括号,减少输出。 - 要将cos内的
*
,^
替换成其他符号,防止后续分割出错。
2.3.5 自定义函数。
自定义函数构建了一个新的类。
public class Function implements Factor {
private final ArrayList<Factor> factors;
private final BigInteger coefficient;
private final int power;
private final String name;
public ArrayList<Number> merge() {
String functionExpr = null;
ArrayList<String> parameters = new ArrayList<>();
for (int i = 0; i < Input.getFunctions().size(); i++) {
if (Input.getFunctions().get(i).getName().equals(name)) {
functionExpr = Input.getFunctions().get(i).cloneFunctionExpr();
parameters = Input.getFunctions().get(i).getParameters();
}
}
if (functionExpr == null) {
System.out.println("error " + name);
}
for (int i = 0; i < parameters.size(); i++) {
ArrayList<Number> list = factors.get(i).merge();
list = Sort.multinomialSort(list);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("(");
stringBuilder.append(Output.getAnswer(list));
stringBuilder.append(")");
assert functionExpr != null;
functionExpr = functionExpr.replaceAll(parameters.get(i),stringBuilder.toString());
}
Lexer lexer = new Lexer(functionExpr);
Parser parser = new Parser(lexer);
Expr expr = parser.parserExpr(1);
ArrayList<Number> ans = expr.merge();
ans = MathMultinomial.multinomialPower(ans, power);
ans = MathMultinomial.multinomialMul(ans, coefficient);
return ans;
}
public Function(String name, ArrayList<Factor> factors, int power, BigInteger coefficient);
}
- name 代表函数名,将该函数名与之前读取的函数民比对,如果比对失败,则报错。
- factor 链表代表传入的参数,将参数挨个配对,替换,最后可以得到完全替换后的表达式。这里也可能先分析表达式,构建一颗树,再替换,
比较麻烦我就没有这样写。 - 最后可以调用现成的
lexer
和Parser
类解析表达式。
2.4 遇到的bug
这次强侧和互测也没有测出我的bug,这里展示我自己测试遇到的bug。
2.4.1 sin(0)
之前程序特判sin内的表达式为0无脑输出0,出现了 sin(0)**0 = 0
的错误。
2.4.2 cos 内表达式的处理。
要将cos 内的 *
, ^
等进行处理,否则后面进行化简分割时会出现大错误。
2.5 复杂度分析
- 方法复杂度
新增了对cos,sin,自定义函数的解析,因子的解析方法有一点超标了。
- 类复杂度
将输出函数转移到了 Output
类,让 。Output
类成功超标
3.Homework3
本次作业要求实现求导因子以及自定义函数嵌套,由于之前框架构建的好,第三次作业是完成得最快的一次作业,半天不到就完成了,可以说是最简单的一次了。
3.1 UML类图
和之前变化不大,增加了一个 Derivation
类。同时更加清晰的展示的数据结构。
3.2 难点分析
本次作业较为简单,之前框架搭建的好的话,难点只有一些容易出错的小细节。比如要提前解析表达书,警惕自定义函数错误替换等。
3.3 实现过程
Parser
方法同样需要加入对求导因子的解析,不过很简单,这里不赘述了。之前讲过的部分同样不再赘诉。
3.3.1 求导因子
public class Derivation implements Factor {
private final Expr expr;
private final BigInteger coefficient;
private final int power;
private final String valuable;
public Derivation(String valuable, Expr expr, int power, BigInteger coefficient);
public ArrayList<Number> merge();
public ArrayList<Number> derive(String valuable);
}
- valuable代表传入的变量。
merge()
方法就是调用expr 的derive()
的方法。不过这里推荐先调用Output
,Parser
,lexer
方法将表达式化简,简化计算。否则derive()
方法会很复杂。
3.3.2 derive()
方法的实现
和 merge()
一样 derive()
方法也是接口 Factor
的一个抽象方法。一样通过递归实现,不够数学关系有所不同。
-
递归到
Number
类时,如果name与传入的变量相同,则将power减小1返回,系数要乘power返回。如果name和传入变量不相等,直接返回0,对于次数为0的要进行特判。 -
递归到
Cos
或者Sin
类时,可以利用对方的构造方法,快速构造。数学关系如下。同样要对pow为0要特判
d ( c o s ( F a c t o r ) p o w ) = d ( F a c t o r ) ∗ ( p o w − 1 ) ∗ ( − s i n ( F a c t o r ) ) ∗ c o s ( F a c t o r ) p o w − 1 d(cos(Factor)^{pow}) = d(Factor)*(pow-1)*(-sin(Factor))*cos(Factor)^{pow-1} d(cos(Factor)pow)=d(Factor)∗(pow−1)∗(−sin(Factor))∗cos(Factor)pow−1
d ( s i n ( F a c t o r ) p o w ) = d ( F a c t o r ) ∗ ( p o w − 1 ) ∗ c o s ( F a c t o r ) ∗ s i n ( F a c t o r ) p o w − 1 d(sin(Factor)^{pow}) = d(Factor)*(pow-1)*cos(Factor)*sin(Factor)^{pow-1} d(sin(Factor)pow)=d(Factor)∗(pow−1)∗cos(Factor)∗sin(Factor)pow−1
3.3.3 derive()
方法的数学关系
同样依赖于 MathMultinomial
方法实现链表的运算。
Expr
由Term
聚合而成
d ( E x p r ) = ∑ d ( T e r m ) d(Expr) = \sum d(Term) d(Expr)=∑d(Term)
Term
由Factor
聚合而成(链式法则)
d ( T e r m ) = ∑ d ( F a c t o r ) ∗ ∏ O t h e r F a c t o r s d(Term) = \sum d(Factor)*\prod OtherFactors d(Term)=∑d(Factor)∗∏OtherFactors
3.4 遇到的bug
本次强侧顺利通过,但是互测被测出来一个bug。
3.4.1 画蛇添足
这是一个不应该出现的小bug,再互测中才被发现。再算出导数时,我画蛇添足的调用Sort
方法对链表进行排序化简,这会出现name为”-1“的情况,这种情况本不应该出现,运算时会将其当作一个变量处理,如果出现该Number
为二次的时候,就会输出-1**2
的情况,这明显是错误的。删除了画蛇添足的排序后,bug就改完了。
3.4.2 提前解析
由于自定义函数支持求导因子,如果直接替换的话,会出现 dsin(x)(sin(x))
的情况,因此需要提前解析化简自定义函数的表达式,使其不含有求导因子。
3.4.3 自定义函数错误替换
由于直接替换参数可能出现错误替换的情况(由于重名,将不应该替换的参数替换),因此参数替换前需要将三个变量替换为没有出现过的字符。但是解析自定义函数表达式的时候,之前和现在的表达式都进行过替换,这样一来相当于没有替换,就会出现错误替换的情况。
3.5 复杂度分析
- 方法复杂度
和之前的差别不大,主要还是优化方法过于复杂。要减少if-else以及for循环的嵌套使用。
- 类复杂度
和之前的差别不大。
体会心得
- 一个好的架构十分重要,在开始写代码前,一定要构思好一共好的结构,这样在后续迭代的过程中往往可以事半功倍。
- 要多和同学交流或者多上网查询资料,java自带的库中,有很多很好用的方法,合理使用不仅仅可以省去重复造轮子的麻烦,也能让代码变得简单易读。虽然我没有上过oopre课程,但是在互联网的帮助下,我很快的就理解了java的基础语法,
虽然很艰辛,但是依旧按时完成了作业。 - 要深刻了解java语言的本质,以免出现深克隆和浅克隆上的错误。
- 在实现功能时,要极可能的避免多层的if-else,for循环的嵌套,可以通过方法来实现,这样不仅仅提高了代码的可读性,也大大降低了debug的难度。、
析自定义函数表达式的时候,之前和现在的表达式都进行过替换,这样一来相当于没有替换,就会出现错误替换的情况。
3.5 复杂度分析
- 方法复杂度
[外链图片转存中…(img-ITScsRRf-1679142573593)]
和之前的差别不大,主要还是优化方法过于复杂。要减少if-else以及for循环的嵌套使用。
- 类复杂度
[外链图片转存中…(img-FkARVNJF-1679142573593)]
和之前的差别不大。
体会心得
- 一个好的架构十分重要,在开始写代码前,一定要构思好一共好的结构,这样在后续迭代的过程中往往可以事半功倍。
- 要多和同学交流或者多上网查询资料,java自带的库中,有很多很好用的方法,合理使用不仅仅可以省去重复造轮子的麻烦,也能让代码变得简单易读。虽然我没有上过oopre课程,但是在互联网的帮助下,我很快的就理解了java的基础语法,
虽然很艰辛,但是依旧按时完成了作业。 - 要深刻了解java语言的本质,以免出现深克隆和浅克隆上的错误。
- 在实现功能时,要极可能的避免多层的if-else,for循环的嵌套,可以通过方法来实现,这样不仅仅提高了代码的可读性,也大大降低了debug的难度。、