【 OO 第一单元总结 】

您所在的位置:网站首页 三角函数转换方法总结 【 OO 第一单元总结 】

【 OO 第一单元总结 】

2023-03-26 15:16| 来源: 网络整理| 查看: 265

「BUAA OO Unit 1 HW4」第一单元总结

目录「BUAA OO Unit 1 HW4」第一单元总结第一次作业UML类图架构分析表达式解析表达式展开表达式计算复杂度分析测试第二次作业UML类图架构分析嵌套多层括号三角函数因子标准项的改变运算过程中的三角函数化自定义函数因子形参和实参的对应关系迷惑的括号问题复杂度分析测试第三次作业UML类图架构分析导!求导思路求导运算调用已定义函数复杂度分析心得体会

第一次作业 UML类图

img

架构分析

读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的多变量表达式,输出恒等变形展开所有括号后的表达式。

表达式解析

数据存储上,依照形式化表达设计为Expr -> Term -> Basic -> Factor ,Parser和Lexer作为工具类,分别承担句法分析和词法分析的功能。

在读取和存储过程中,Parser和Lexer合作,构建了一棵多叉表达式树。

Factor接口为因子类的公共接口,所有的因子都实现了这个接口,由于之后的迭代作业新加了一些因子,因此用公共接口统一管理是一个非常明智的做法,在第三次作业的求导中将会体现这种方法的便捷性。

表达式展开

关于表达式的展开和化简,我在第一次作业中尝试了很多思路,最后在尝试中选择了先转化为后缀表达式再对其进行计算的方法,至于如何生成后缀表达式,我选择重写了toSrting 方法,通过利用项与项之间通过 + 或者 - 连接,项中的各个因子通过 * 连接的特性生成后缀表达式的逻辑是非常简单的,取出两个项或者两个因子然后在后面加个符号。计算方面,由于后缀表达式会把所有带符号的项或者因子分解,所以后缀表达式中只有最基本的因子以及运算符号,由此我发现没有必要对项或者因子进行可运算化的操作,所有的项或因子都可以由其中的基本因子运算生成,比如$$2*(x+1)\quad = \quad x\quad1\quad+\quad 2\quad*\quad$$并不需要将其中的 2 和 (x+1) 转化为可运算的结构化数据来进行乘法运算,而是从 x 和 2 这两个基本因子开始运算来获取整个项的结构化数据。

表达式计算

计算部分我采用了将所有项转换成标准项形式,表达式即为标准项的叠加,具体如下:$$Term\quad = \quad cofficientx^ay^b*z^c\Expr\quad = \quad \sum^n_{i=1}Term$$而为了方便合并同类项,我在实现中采用了大量 HashMap 比如Expr 储存标准项和它的系数我选用的就是HashMap的形式,但为什么Key部分用的是String呢?这个故事说来话长,上手第一周作业是我可以说对Java一窍不通,对HashMap的特性也完全不了解,在多次尝试使用数组或者另一个HashMap作为Key的类型时IDE频频报错,最后选择了使用"x^a*y^b*z^c"的字符串来储存xyz的指数,然后再用正则表达式把指数解析出来。

String pattern = "x\\^(\\d+)\\*y\\^(\\d+)\\*z\\^(\\d+)"; int xp = Integer.parseInt(a.group(1)) + Integer.parseInt(b.group(1)); int yp = Integer.parseInt(a.group(2)) + Integer.parseInt(b.group(2)); int zp = Integer.parseInt(a.group(3)) + Integer.parseInt(b.group(3)); String index = "x^" + xp + "*" + "y^" + yp + "*" + "z^" + zp;

这个方法虽然确实是管用而且符合直觉,但是不符合面向对象的设计思想,在后续迭代中被优化了。

复杂度分析

​ 方法的衡量指标:

CogC(Cognitive complexity)认知复杂度:衡量一个方法的控制流程有多困难去理解。具有高认知复杂度的方法将难以维护。sonar要求复杂度要在15以下。计算的大致思路是统计方法中控制流程语句的个数ev(G)(essential cyclomatic complexity):方法的基本圈复杂度,衡量程序非结构化程度的。iv(G) (Design complexity):设计复杂度v(G)(cyclomatic complexity):方法的圈复杂度,衡量判断模块的复杂度。数值越高说明独立路径越多,测试完备的难度越大。

第一次作业复杂度分析见下图:

img

其中 Item 中的 getUp() 方法复杂度较高,这个方法是用来构建最后输出的字符串的,其中涉及了很多特判并且我并没有把化简单独作为一个方法或者类而是合并再来输出中,导致这个方法复杂度很高。Lexer 中的 preProcess()方法也是在预处理输入字符串的过程中进行了很多特判复杂度比较高。MainClass 中的 simplify()方法就是具体的计算过程,调用了加减乘除的计算方法,并且涉及旧项的删除和新项的增加的过程都是复制粘贴的,并没有创建另一个方法,导致该方法复杂度高。

测试

本次作业在中测和强测中没有出现bug,在互测中出现bug。

其中,互测的bug在于表达式的次数太高会导致TLE,产生这个bug的原因是在做乘法运算时相乘项太多,但其中很多其实是系数为0的无关项,不需要考虑,因此在乘法运算时加入对该项系数的判断就能减少运算时间。

第二次作业 UML类图

img

架构分析

在第一次作业基础上,本次迭代作业增加了以下几点:

本次作业支持嵌套多层括号 。本次作业新增三角函数因子,三角函数括号内部包含任意因子。本次作业新增自定义函数因子,但自定义函数的函数表达式中不会调用其他函数。 嵌套多层括号

在上一次作业中我选择了后缀化表达式的方法,因此计算的优先级是非常清晰的,因此多层嵌套括号问题直接迎刃而解。

三角函数因子 标准项的改变

因为新增了三角函数因子,标准项的形式也发生了改变,具体改变如下$$Term\quad = \quad cofficientx^ay^bz^c\prod^n_{i=1}sin^d(Expr_i)*\prod^n_{j=1}cos^e(Expr_j)\Expr\quad = \quad \sum^n_{k=1}Term_k$$由于$sin()$和$cos()$因子在一个项中的数量不能确定,因此不选用容器存储看起来会比较复杂,因此我选择了又一个HashMap来储存所有的$sin()$或者$cos()$因子。Key是括号中的表达式,Value是三角函数因子的指数。那么问题又来了,在合并三角函数同类项时,如果三角函数括号内的内容是$Expr$类型很难判断是否完全一致,因此我复用了第一次作业中用来构建最后输出字符串的方法getUp()将括号中的$Expr$转换成字符串,这样做又有一个小优点就是通过统一的getUp()过程,两个$Expr$只要内部的抽象结构相同,哪怕内部元素顺序不同,最后生成的字符串一定是一致的。所以最后用来存储三角函数因子的容器是HashMap 。

运算过程中的三角函数化

由于在第一次作业中我使用了后缀表达式,而且后缀表达式运算逻辑清晰简洁,所以三角函数运算我同样也选用后缀表达式的形式解决。但正如我之前所说的,后缀表达式计算的核心是生成式计算,即由几个基本的因子生成一整个表达式的过程,那么$sin(Expr)$该如何生成呢?我选择的方法是先依据第一次作业的方法生成$Expr$然后对其进行$sin$化。例如:$$sin(x+1)\quad=\quad x\quad 1\quad +\quad sin$$这里把$sin$当做一个单目运算符,对前一个生成表达式进行计算。

结合之前所提到的三角函数储存形式,具体的代码实现思路是把一个标准项构成的表达式转化为字符串,存入$sin()$的HashMap中,然后把其他属性都清零。

newPoly.getSin().put("(" + sinContent + ")", new BigInteger("1")); polyTerms.clear(); polyTerms.put(newPoly, new BigInteger("1")); 自定义函数因子

关于自定义函数,我的处理方式是在对输入字符串解析前对其进行预处理,即将输入字符串中所有的自定义函数调用全部根据它们的参数进行代入,使得解析时的表达式没有自定义函数,接着像之前的方法对其进行展开。

形参和实参的对应关系

在实现自定义函数时,由于题目规定可以如$f(y,z,x)$的形式进行函数定义,因此需要记录形参和实参的顺序才能在调用时得到正确的结果。对此我使用的方法还是使用HashMap用Key来记录形参,Value来记录实参构建映射关系。在调用函数时,根据形参和实参的对应关系进行字符串替换。

public String Replace(CustomFunc i) { Set formalArgSet = i.args.keySet(); String res = i.format; for (String formalArg : formalArgSet) { res = res.replace(formalArg, i.args.get(formalArg)); } return res 迷惑的括号问题

在我完成第一版代码后,我发现当自定义函数的参数是普通变量时运行结果是正确的,一旦参数是三角函数就会出现一些奇怪的bug,有时是运行时错误,有时是结果错误。在排查后发现问题出在函数调用时的实参解析,一开始我的想法是先读取函数调用的括号内容,再以","分隔这部分内容就能得到实参,但碰到形如$f(y,sin(x))$这种调用时,正则表达式匹配的部分是$(y,sin(x)$,显然得到的实参将会是 $y$ 和 $sin(x$ ,括号不匹配会导致很多可能的错误。解决方法也不难,建立一个括号栈来实现括号匹配就行。虽然这个bug比较普通,但是它体现出字符串替换这种方法虽然简单省力,但不够严谨和结构化的问题。因此推荐的做法还是在Parser解析器中建立解析自定义函数调用的专门方法。

复杂度分析

img

可以看出PolyTerm.mul()方法的复杂度非常高,因为在两个标准项相乘时要涉及三角函数内容是否有重复的判断,而且我的$sin()$和$cos()$是分开写的,因此复杂度惊人的高,后来想到三角函数的乘法可以单独抽象成一个方法来减少此方法的复杂度。getUp()方法由于加入了三角函数因子和更多的特判复杂度也水涨船高,可以通过把字符串的构建和化简分开来降低复杂度。同时自定义函数的Process()方法由于调用了多个方法复杂度也比较高。

测试

本次作业在中测没有出现bug但在强测和互测中出现了bug。

其中强测和互测的bug是同质化bug,问题都出在了PolyTerm.mul()方法,内容相同的三角函数相乘没有幂次相加,这也印证了圈复杂度高的方法容易出错。

在hack的过程中,我发现有些同学读入$sin(x-x)$化简后会因为括号内的内容为0而输出$sin()$这种括号内为空的表达式。除此之外有些同学在处理自定义函数时没有化简直接字符串替换,会导致替换后的字符串不合文法。

第三次作业 UML类图

img

架构分析

在第一次作业基础上,本次迭代作业增加了以下几点:

本次作业支持求导操作,新增求导算子 。根据第三部分形式化表述,求导因子可以出现在很多位置,包括函数调用实参,函数定义表达式,三角函数内部等,注意考虑周全。 本次作业函数表达式中支持调用其他“已定义的”函数。 导! 求导思路

求导的基本思路在指导书中已经给出了提示,主要分为三部分:

对表达式中的各个项单独求导再相加根据乘法法则分别对项中的各个因子单独求导其余部分不变,最后再相加根据链式法则对因子中嵌套函数进行求导,直到求导求到基本因子

$$de(Expr) = de(Term_1)+de(Term_2)+......+de(Term_n)\de(Term) = de(Factor_1)Factor_2......Factor_n\+Factor_1de(Factor_2)......Factor_n\+......\+Factor_1Factor_2......*de(Factor_n)\$$

代码实现部分我采用了类似递归下降的思路建立了deExpr,deTerm,deFactor三种方法层层调用,代码量不算很多,体现出了这个架构的特色。

求导运算

求导运算最重要的其实是对各种因子的求导,因为我对求导也采用了递归下降的思路,所以最后要求导的表达式都是转化为最基本因子求导结果的运算组合。例如$de(xy + 2x+1)\quad\rightarrow\quad de(x)*y+de(y)*x+de(2)*x+de(x)*2+de(1)$

而这最核心的对基本因子求导过程,我是通过 factor instanceof whichType 实现的。之前抽象化的那么多类也在这里派上了用场。

if (factor instanceof Constant) { return "0"; } else if (factor instanceof Expression) { Expression exprFactor = (Expression) factor; return deExpr(exprFactor); } else if (factor instanceof Variable) { Variable variable = (Variable) factor; return deVariable } else if (factor instanceof PowerFunc) { PowerFunc powerfunc = (PowerFunc) factor; return dePowerFunc } else if (factor instanceof TriFunc) { TriFunc trifunc = (TriFunc) factor; return deTriFunc } else { return "0"; } 调用已定义函数

在自定义函数时调用已定义的函数的思路很简单,在上一次作业中,我使用了ArrayList来储存已定义的自定义函数,因此在下一个函数定义时调用其实和在最后的表达式中调用没什么区别,无非就是遍历一遍ArrayList来寻找调用的函数的定义,然后进行一个字符串替换即可。

复杂度分析

img

与上一次作业相比,这一次作业的复杂方法并没有改变因为懒,还新增了一个复杂方法DeFunc.defactor() 这是由于在对各种因子求导时我对各种因子分别进行了判断,有些因子比如幂函数因子求导比较复杂,堆砌起来就导致整个方法复杂度比较高,其实可以新建一些专门的方法来对复杂因子进行解析。

心得体会

由于没有上过OO先导课,第一次作业对我来说是及其痛苦的,一个语法一个语法地查使得我作业效率极低,期间一度想放弃还找到助教gg哭诉,但最后还是赶在ddl之前完成了第一次作业,甚至结果还不错。虽然这段经历很折磨,但是我觉得我从中获取的知识以及面对复杂问题的抗压能力和解决能力还是弥足珍贵的,之后两次作业再有了第一次作业的铺垫后就变得得心应手起来,在合理的时间规划下,OO作业甚至变得挺有乐趣。

在艰难完成第一次作业之后,我其实并没有领会到面向对象这一思想的精髓,我定义的那么多类不过是照猫画虎。但在第二次尤其是第三次作业的迭代过程中我对一个个因子求导时我才体会到面向对象的清晰明了,而且我的代码确实还有很大的改进空间,有些方法因为架构设计上的偷懒格外冗长,debug时也很难找出错误。希望在下一次单元的训练中能够交出一份令我自己满意的作业。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3