OO_Lab0

问题描述

对表达式结构进行建模,将表达式中非必要的括号进行展开并化简。

设定的形式化表述(仅写出部分):

表达式 \(\rightarrow\) 空白项 [加减 空白项] 项 空白项 | 表达式 加减 空白项 项 空白项

项 \(\rightarrow\) [加减 空白项] 因子 | 项 空白项 '*' 空白项 因子

因子 \(\rightarrow\) 变量因子 | 常数因子 | 表达式因子

变量因子 \(\rightarrow\) 幂函数 | 三角函数 | 自定义函数调用 | 求和函数

表达式因子 \(\rightarrow\) '(' 表达式 ')' [空白项 指数]

三角函数 \(\rightarrow\) 'sin' 空白项 '(' 空白项 因子 空白项 ')' [空白项 指数] | 'cos' 空白项 '(' 空白项 因子 空白项 ')' [空白项 指数]

解决思路

整个问题的解决可以分成两部分,一是从字符串分析出表达式结构并通过合适的方式保存,二是将非必要的括号展开并进行化简,得到只有必要括号的表达式。

分析字符串结构可以采用递归下降的方法,从设定的形式化表述出发,分别建立处理表达式、项、因子、变量因子、常数因子、表达式因子的操作。

以处理表达式为例,表达式的表述为:

表达式 \(\to\) 空白项 [加减 空白项] 项 空白项 | 表达式 加减 空白项 项 空白项

前一种情况很容易处理,考虑第二种情况,我们要找出“表达式”、“加减”、“项”三部分,因此我们可以找出倒数第二项后的第一个加减号将其作为划分(因为可能存在多个加减号),两部分分别按表达式和项各自处理。其他的情况也都可以类似处理,完全从形式化表述出发,形成一个天然的递归结构。这一做法实质是找到整个表达式中优先级最低的运算符,将左右两边作为表达式递归运算,构造了一个类似表达式树的结构,但是在实际操作中并不需要真的建树,只要每个表达式返回一个已化简的结果即可。

展开括号也可以采用类似的形式,分别建立表达式、单项式、幂函数、三角函数的元素类,每个类均返回处理完成的表达式结果,定义表达式的加减乘操作即可完成括号的展开。

如下为我的UML类图(简化了一些只在类内使用的函数):

架构分析

优点

  1. 思路清晰:本架构完全从形式化表述出发,在进行程序设计时,不需要设计特殊的操作(如设计形式化表述对应的正则表达式),不需要纠结是否有特殊情况没有考虑。在实际编写中,大多数情况下只要按照要求制作即可,一方面减少了编写难度,另一方面减少了错误的可能。
  2. 鲁棒性强:我们可以看到,在本题目中(尤其是前两个题目)为了降低难度而增加了诸多限制,如不存在嵌套括号等,但是利用这种架构,只要输入的表达式符合形式化表述,那么他就可以被解析。在本作业中,我从第二次作业到第三次作业只进行了极小的修改,按照第三次作业的要求对输出进行了一些特殊处理,原本的处理结构没有任何变化,这也足以体现这一架构的通用性。

缺点

结构破碎,这一点也可以从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的函数在代码上更加简洁,ExprMonomial中主要函数有两部分,一是加减乘等运算;二是simplifysubstitute,这一部分可能是我认为这次作业唯一一次体现“对象”、“行为”等概念的地方了,这样的结果就是我最后的UML类图如上所示几乎没有什么结构可言。在这方面有独到理解的欢迎和我多探讨。

最后,感谢各位助教的付出,感谢和我交流的同学,期待之后的OO课程。

OO_Lab0总结博客的更多相关文章

  1. Android请求网络共通类——Hi_博客 Android App 开发笔记

    今天 ,来分享一下 ,一个博客App的开发过程,以前也没开发过这种类型App 的经验,求大神们轻点喷. 首先我们要创建一个Andriod 项目 因为要从网络请求数据所以我们先来一个请求网络的共通类. ...

  2. 一步步开发自己的博客 .NET版(11、Web.config文件的读取和修改)

    Web.config的读取 对于Web.config的读取大家都很属性了.平时我们用得比较多的就是appSettings节点下配置.如: 我们对应的代码是: = ConfigurationManage ...

  3. 一步步开发自己的博客 .NET版(10、前端对话框和消息框的实现)

    关于前端对话框.消息框的优秀插件多不胜数.造轮子是为了更好的使用轮子,并不是说自己造的轮子肯定好.所以,这个博客系统基本上都是自己实现的,包括日志记录.响应式布局.评论功能等等一些本可以使用插件的.好 ...

  4. 【原】Github+Hexo+NextT搭建个人博客

    摘要 GitHub 是一个开源项目的托管网站,相信很多人都听过.在上面有很多高质量的项目代码,我们也可以把自己的项目代码托管到GitHub,与朋友们共享交流.GitHub Pages 是Github为 ...

  5. 我为什么要写LeetCode的博客?

    # 增强学习成果 有一个研究成果,在学习中传授他人知识和讨论是最高效的做法,而看书则是最低效的做法(具体研究成果没找到地址).我写LeetCode博客主要目的是增强学习成果.当然,我也想出名,然而不知 ...

  6. 博客使用BOS上传图片

    1.博客平台的选定 从大学开始做个人主页算起,最开始是使用html,CSSS写简单的页面,后面大学毕业之后接触到了WordPress,就开始用WordPress搭建网站.现在还维护着一个农村网站.ht ...

  7. 在jekyll模板博客中添加网易云模块

    最近使用GitHub Pages + Jekyll 搭建了个人博客,作为一名重度音乐患者,博客里面可以不配图,但是不能不配音乐啊. 遂在博客里面引入了网易云模块,这里要感谢网易云的分享机制,对开发者非 ...

  8. iOS controller解耦探究实现——第一次写博客

    大学时曾经做过android的开发,目前的工作是iOS的开发.之前自己记录东西都是通过自己比较喜欢的笔记类的应用记录下了.直到前段时一个哥们拉着我注册了一个博客.现在终于想明白了,博客这个东西受众会稍 ...

  9. 中文 iOS/Mac 开发博客列表

    中文 iOS/Mac 开发博客列表 博客地址 RSS地址 OneV's Den http://onevcat.com/atom.xml 一只魔法师的工坊 http://blog.ibireme.com ...

  10. 企业shell面试题:获取51CTO博客列表倒序排序考试题

    #!/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin HTMLFILE=/home/oldboy/ht ...

随机推荐

  1. 从各种点理解Unity中的协程

    这个写的很好,https://zhuanlan.zhihu.com/p/59619632

  2. [*]Quadratic Residual Networks: A New Class of Neural Networks for Solving Forward and Inverse Problems in Physics Involving PDEs

    Accepted by SIAM International Conference on Data Mining (SDM21) 本文提出了二次残差网络,通过在应用激活函数之前,添加二次残差项到输入的 ...

  3. pip备份、安装requirements.txt中的包和anaconda的安装(linux)

    pip备份.安装requirements.txt中的包和anaconda的安装(linux)   1. 从已有的环境中,备份已经安装的package pip freeze > requireme ...

  4. stream 链式结构

    Double totalPaymentAmount = Optional.ofNullable(wayfairMonthBill.getPaymentAmountDetailJson()) .filt ...

  5. 关于flex元素超出父元素的解决方法

    左边是label, 右边是input. 设置父级为display:flex; input为flex:1; 然后label 为 white-space: nowrap; 这时input就有可能超出父级. ...

  6. js循环中删除数组中的某个元素

    (1)使用js中的splice方法循环删除数组中某个值 eg: var arr=new Array(); arr.push(1); arr.push(2); arr.push(3); arr.push ...

  7. React脚手架的使用

    初始化项目 npx create-react-app my-app // 或 npm init react-app my-app // 或 yarn create react-app my-app 启 ...

  8. chrome 请停用以开发者

    链接:https://pan.baidu.com/s/1YhWINGlUVyTE5XyBVIGW_Q 提取码:23t2 转载至   https://www.cnblogs.com/it-tsz/p/9 ...

  9. Navicat连接Oracle时报错ORA-28547:完美解决

    1. 先用你的IDEA或者别人的连接到oracle数据库(为了查询版本) 1.1 查询版本SQL:select * from v$version; 2. 引入对应的oci.dll文件 链接:https ...

  10. Django中的缓存的配置与使用

    一.使用装饰器 在文件开头导入下面代码,然后在函数上方添加缓存的装饰器 from django.views.decorators.cache import cache_page 一:在视图View中使 ...