BUAA_OO_2022 第一单元总结
BUAA_OO_2022 第一单元总结
1.架构设计
架构流程:
输入 -----> 预处理 -----> 拆项建树 -----> 合并 -----> 化简 -----> 输出
主体架构:
我的第一单元三次作业的核心思路都是建立二叉树,以操作符为结点,以两个操作数作为子节点。
以“ x ^ 3 + ( x ^ 2 + x + 1) * ( x + 2 ) ”为例,进行拆项建树:
- 第一次作业的操作符只有 + 和 *,叶结点也只能为 x 或 常数,拆项一拆到底。
- 第二次作业,操作符不变,但是叶结点扩充至表达式因子、三角函数因子等 。
- 第三次作业中操作符增加了 **, 解决幂指数较大递归多次的问题。
合并时,自底向上,通过每个操作符,将子结点链接起来。
在第一次作业中,Variable类和Constant类聊胜于无,功能集中在Expression类和Operator类中(面向对象,面向过程!)
在第二次作业中,正式步入面向对象。对于求和函数和自定义函数,采用了预处理替换的方式,简单直接但不利于后续迭代开发。
在第三次作业中,将自定义函数和三角函数都作为因子处理,并在相应的类中,代换后作为表达式处理,建立分支树。
细节实现:
-
建树:
采用递归的方式建树,依次寻找 +、*、 **三个操作符
public void buildTree(String expr) { if(getPlus(expr) > 0) { ... buildTree(expr.subString(getPlus(expr))); } else if(getMul(expr)) { ... buildTree(expr.subString(getMul(expr))); } ... }
-
化简:
对于以加号/减号分隔的每一项:
-
以BigInteger类型的constant保存系数。
-
以int类型的power保存x的幂指数。
-
以HashMap<String, Integer>类型的triangle保存三角函数及其指数,之后对三角函数降幂排列。
-
以HashMap<String, BigInteger>类型的coefficient保存这一项。
再通过对三角函数内部表达式降幂排列,每一项构造出如下形式。
3*x**2*-1*cos(x**2)*sin(x**2)**2
constant = -6;
power = 2;
triangle.put("cos(x**2)", 1);
triangle.put("sin(x**2)", 2);
String term = -x**2*sin(x**2)**2*cos(x**2);
coefficient.put(term, constant);
通过coefficient索引表达式,获取系数进行合并。
2.程序结构度量
方法复杂度:(仅列出较为重要的类)
方法 | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Function.getSpilt(String) | 14.0 | 1.0 | 6.0 | 7.0 |
Sum.getSpilt(String) | 14.0 | 1.0 | 6.0 | 7.0 |
Simplify.getSplit(String) | 14.0 | 1.0 | 6.0 | 7.0 |
Expression.getPower(String) | 9.0 | 3.0 | 5.0 | 6.0 |
Expression.buildBranch(String) | 8.0 | 1.0 | 9.0 | 9.0 |
Simplify.merge() | 8.0 | 1.0 | 5.0 | 5.0 |
Expression.getPlus(String) | 8.0 | 4.0 | 5.0 | 6.0 |
Operator.polyMul(String, String) | 7.0 | 1.0 | 5.0 | 5.0 |
Constant.Constant(String) | 7.0 | 1.0 | 4.0 | 4.0 |
Triangle.Triangle(String, HashMap) | 7.0 | 1.0 | 6.0 | 6.0 |
Simplify.triangleSort(HashMap) | 7.0 | 1.0 | 5.0 | 5.0 |
Simplify.powerSort() | 6.0 | 1.0 | 4.0 | 4.0 |
Expression.getMul(String) | 6.0 | 4.0 | 3.0 | 4.0 |
Expression.getPos(int, String) | 4.0 | 1.0 | 3.0 | 4.0 |
Simplify.getPower(String) | 3.0 | 3.0 | 2.0 | 3.0 |
Expression.powerType(int, String, int) | 3.0 | 1.0 | 4.0 | 4.0 |
Operator.getAnswer() | 3.0 | 1.0 | 4.0 | 5.0 |
Triangle.getPower(String) | 2.0 | 2.0 | 2.0 | 2.0 |
Sum.Sum(String, HashMap) | 1.0 | 1.0 | 2.0 | 2.0 |
Constant.getBase(String) | 1.0 | 1.0 | 2.0 | 2.0 |
MainClass.main(String[]) | 1.0 | 1.0 | 2.0 | 2.0 |
Constant.getPower(String) | 1.0 | 1.0 | 2.0 | 2.0 |
Function.Function(String, HashMap) | 1.0 | 1.0 | 2.0 | 2.0 |
Average | 4.09 | 1.19 | 2.89 | 3.12 |
相较于递归下降方法,我建树的思路的方法复杂度和类复杂度的数值都较高,因为在建树的过程中,使用了很多的if-else语句去进行分支判断,从而判断拆分的每一项属于哪种类型或因子。
类复杂度:
类 | OCavg | OCmax | WMC |
---|---|---|---|
Constant | 1.83 | 4.0 | 11.0 |
Expression | 4.1 | 10.0 | 41.0 |
Function | 2.4 | 7.0 | 12.0 |
MainClass | 1.5 | 2.0 | 3.0 |
Operator | 3.33 | 7.0 | 20.0 |
Simplify | 4.9 | 15.0 | 49.0 |
Sum | 2.4 | 7.0 | 12.0 |
Term | 1.0 | 1.0 | 4.0 |
Triangle | 2.2 | 6.0 | 11.0 |
Variable | 1.0 | 1.0 | 4.0 |
Average | 2.93 | 6.0 | 16.7 |
对于自定义函数、三角函数类、括号类,需要紧密依赖Expression类,继续拆项,出现了和表达式类自圈的情况,类似于状态机如下图。导致这几个类的耦合度很高,代码复杂度也很高。
类图
3.个人bug分析
第一次作业
编号 | bug描述 | bug位置 | 总行数:未修复/修复 | 复杂度:未修复/修复 |
---|---|---|---|---|
1 | 化简处理时,直接replace("+1", ""),忽略11x的情况 | 表达式类 | 619/625 | 2.46/2.48 |
2 | 添加结点出错,导致ArrayList索引越界 | 表达式类 | 619/625 | 2.46/2.48 |
3 | 括号处理异常,会把-(x-x)处理为(-x-x) | 表达式类 | 619/625 | 2.46/2.48 |
4 | 乘法处理错误,两个项相乘,中间直接加上了* | 操作符类 | 619/625 | 2.46/2.48 |
第一次作业的bug主要是出在括号项的处理上,括号项的乘法以及括号项前的正负号没有得到很好的处理。
第二次作业
编号 | bug描述 | bug位置 | 总行数:未修复/修复 | 复杂度:未修复/修复 |
---|---|---|---|---|
1 | 如f(x)=x**2,f(-2)带入时未给2带上括号 | 主类 | 606/614 | 3.24/3.24 |
2 | 未注意幂有前导0的情况,直接使用了replace("x**0", 1) | 主类 | 606/614 | 3.24/3.24 |
3 | 误以为幂次不超过10,使用replace("**1", "")的方式替换 | 化简类中 | 606/614 | 3.24/3.24 |
第二次作业的bug主要是出在化简和自定义函数的处理上。化简时,草率的将x0和x1两种情况直接化简,未考虑前导零以及幂指数超过10的情况。自定义函数bug的修复较大,在三角函数类中需要处理冗余的括号以及内部化简,之前的设计是只处理内部常数的幂函数的化简,修复后三角函数内部作为expression处理。
第三次作业
编号 | bug描述 | bug位置 | 总行数:未修复/修复 | 复杂度:未修复/修复 |
---|---|---|---|---|
1 | 未考虑s < e的情况 | 求和函数类 | 791/795 | 2.93/2.95 |
2 | 求和函数中-i的处理有误,负号会被直接忽略 | 求和函数类 | 791/795 | 2.93/2.95 |
3 | 输出格式有误,sin(sin(1)*cos(1))缺少括号 | 三角函数类 | 791/795 | 2.93/2.95 |
第三次作业的问题主要是忽略了求和函数 s < e的情况。
总体分析
总体而言,行数和圈复杂度的对比,也说明程序的bug主要还是集中在一些细节上。实际上,三次作业的大部分bug,都是在不断优化(卷性能分)中产生的,但是一些细节没有考虑周到,导致了bug的出现。
4.互测策略
在对自己的作业进行测试时,主要归纳出了以下几个测试方向。
-
正负号。
-
空格、缩进。
-
幂指数前导0。使用如x**002这类数据测试。
-
大数。求和函数中的s、e容易被忽略。
-
求和函数s > e。这是指导书中的细节,规定了s > e 时,求和结果应为0。
-
求和函数的i替换。求和函数对表达式中的i替换时,会把sin中的i也替换掉。
此外,如f(x)=x**2,f(-2)这种类型也很容易出现问题。但是在互测中不允许出现自定函数,这类情况只能用于自测。
5.心得体会(自刀)
-
不要刻意卷性能分!!!绝大部分的bug都是因为卷性能分产生的(为了20分性能分丢80分功能分显然亏本)。 -
拒绝面向对象!!!第一次作业,成功被我写成了面向过程,代码可读性以及结构的鲁棒性极差。(不能理解为什么面向过程的代码复杂度最低) -
为代码的后续迭代做好准备!!!在第二次作业中,我将求和函数和自定义函数在预处理时替换,并将幂函数和三角函数放在用一个类中。在第二次作业限制较多的情况下,这样做完全是可以的,但是却不利于后续迭代,导致第三次作业几乎是完全重构。
-
遵循高内聚低耦合的原则!!!耦合度高的代码块在debug时堪称灾难~~~代码的高内聚做的也不好,经常性一个方法几十行,导致方法的复杂度很高,debug牵一发动全身。之后的设计还是应该细致拆分,高内聚低耦合!!!