OO第一单元总结
第一次作业总结
思路
第一次作业的题面很简单,就是一个多项式化简问题,但是实际操作起来远比我想象的要复杂。
我的基础思路是两步,第一步是解析,第二步是计算。
解析
在第一步中,我具体使用了递归下降法来解析式子,并最终将其转化为后缀表达式来方便接下来的运算
- 首先对表达式预处理
- 再对表达式的每个字符挨个进行词法分析,变为token列表
- 再对token列表进行语法分析,解析为各种对象
- 最后将其输出为一个后缀表达式
计算
第二步就是对后缀表达式的计算与化简
- 先挨个读入对象,判断类型
- 是操作数就存储,是运算符就取出最近的两个操作数进行计算
- 最后化简得到结果
具体实现
- Token类

在进入词法分析前,还有一个前置要求,就是token类的定义,这是我们识别表达式中内容的最小语法单元,其中定义了ADD,SUB,MUL,LPAREN,RPAREN,NUM,POW几种类型,能够识别出表达式中可能出现的所有字符。
另外还配有getType与getContent方法来获得对应的值。
- Lexer类

预处理
我在第一次作业中将这一块放在了Lexer类之中,对应其中的Pre型方法,进行了较为繁琐的预处理操作,包括去除空格,对连续符号的处理("+-","^+","(-"......),前导0的处理,指数的展开(在这次作业中我采取的是将有指数的表达式在预处理时全部展开的方式来消除指数,也埋下隐患)等等。
词法分析
将预处理之后的表达式进行处理,将其转化为由一个个token组成的列表,使得在语法分析时能够每次都输出一个操作数或运算符。
NumPow_Handle方法用于处理转化为Pow类型Token,这个处理比运算符复杂,故单列出来。
- Parser类

- 使用递归下降法处理表达式,递归地分别对表达式(Expression)、项(Term)、因子(Factor)进行处理。
- 对于表达式和项的解析都是调用下级,比较简单。在对因子解析的时候,要考虑到因子的多种可能以及解析处理
- 在parserTerm2中,我以处理好的项的所有因子为模板,构建了一个多项式Poly的Arraylist用以储存除了表达式因子之外的因子,用于在计算中进行比较。
- Expression类

- 存储表达式,包括项和符号
- 重写toString方法,使其按照后缀表达式要求输出。
- Term类

- 存储项,包括所有因子factor,所有多项式因子base,以及系数cofe
- 重写toString方法,使其按照后缀表达式要求输出。
- 有更改系数的方法changeCofe
- Poly类

- 存储常量因子,变量因子,将两者统一写成底数与指数的形式,方便处理。
- 重写toString方法,使其按照要求输出。
- 有更改系数的方法changeCofe与有更改底数的方法changeBase
- Factor类

- 接口,下接Expression类和Poly类
- 定义三个方法(这是我处理的不好的一个地方,Expression类不会用到两个change方法)
- Calc类

包含所有与计算相关的处理
主要分为两类,加减法与乘法,整体由calcExpr调用。
Add与Sub是加减法运算的主要方法,下属CompareTerm_AS,CompareTerm_AS1,choose_AS,handle_AS。
CompareTerm_AS与CompareTerm_AS1是对进行加减法的两个项是否相等进行判断。choose_AS是根据两项的大小判断两项进行的运算。handle_AS是对运算结果数据的处理。
Mult是乘法运算的主要方法,下属CompareTerm_M,且调用了Add方法。
CompareTerm_M是对进行乘法的两个项是否相等进行判断。
- Print类

- 最后的输出处理
- 定义了相关因子的输出方式,以及输出优化处理
基于度量的程序结构分析
- 总UML图

- 代码行数

- 类复杂度

- 可以看到Lexer类,Parser类,Calc类,Print类的复杂度较高。这几个类承担了较重的任务,使用了较多的循环来达成目的,导致复杂度较高
- 方法复杂度


- 在方法中,关于计算的方法复杂度较高,还有就是对表达式进行预处理与输出化简处理的方法复杂度较高。这些方法在实现时使用了较多的循环,一个是用来遍历含有因子,一个是遍历字符化简,因此复杂度较高
bug分析
第一次的错误主要是两个,一个是优化时的问题,一个是加减法符号的问题。
- 优化的问题是对于"*1","+0"等的处理,我是直接将其删除,但是对于像
x*160这样的式子就会被我化简为x60,从而产生问题 - 加减法符号的问题就比较严重,也是导致我第一次强测惨淡的原因。针对这个问题,我将我处理加减法的逻辑与方法进行了重构。在原来,我的每个项与因子都有符号,导致计算时,我既要考虑因子的符号,还有考虑其所在项的符号。重写后,我将因子的符号抛弃,使只有项保留符号,计算时只考虑项的符号,算完后有只改变项的符号,从而避免了繁琐的形式带来的意想不到的错误。
第二次作业总结
思路
本次作业新增加的内容有三个,一个是支持嵌套括号,一个是指数函数,还有一个是自定义函数。
- 对于嵌套括号,我没有额外进行处理,递归下降能够对其进行解析,不需要额外考虑。
- 对于指数函数,这相当于是新加入了一种基础因子,且其无法与之前的常量与变量相统一,因此需要额外进行处理。
- 对于自定义函数,需要关注的是一个进行替换的问题,需要考虑相关的策略。
具体实现
相较于第一次作业,这次主要增添了三个类,修改了几个类
- Expon类

存储指数函数,存有其指数,与括号中的表达式,其中formula为其字符串形式,expression为其解析为表达式的形式。前者方便输出,后者方便比较。
配有改变index与formula的方法
配有解析Expon的方法,目的是从formula解析出expression
配有输出方法toString,CalcExpon方法用于输出时化简formula
- Func类

- FuncHandle类

- 存储有所有解析完成的自定义函数
- funcin方法用于录入函数,解析自定义函数的表达式与自变量。
- funcout,funcout1方法用于替换自定义函数,将表达式中的自定义函数替换为带入实参后的值
- isFunc为判断是否为函数的方法
修改的类
- Token类加入Exp与Func类型
- Lexer中加入对Expon与function的解析
- Parser中加入对Expon与function的解析
- Calc类的计算逻辑中加入了有关Expon的相关内容
基于度量的程序结构分析
- 总UML图

- 代码行数

- 可以看到相较于第一次作业来说,本较为复杂的几个类仍然有着较高复杂度,可以看到新增加的操作对于Lexer类的影响较大,增添了较多代码;其他类都增添不多,约30行左右。新加入的几个类中,Expon类代码较多,因为其涉及到了Expon的较多处理;Funchandle类较为复杂,其涉及到了函数的录入,替换,输出等等操作。
- 类复杂度

- 除去原有的类之外,Funchandle类较为复杂。其中对于自定义函数录入的解析,对于实参与形参的替换等等导致其复杂度偏高,尤其是替换,我采用的是字符串替换,会对表达式进行多次遍历,增加了复杂度。
- 方法复杂度



- 可以看到除去第一次作业的方法,新加入的方法复杂度都不高,原因是其并没有新添较多复杂的方法。但是原来较为复杂的方法在加入指数函数与自定义函数后变得更为复杂,因为其处理相关内容是需要考虑新加入的因素,尤其是在进行计算时,需要考虑指数函数带来的对比较等操作的影响。
bug分析
本次bug主要分为三个,其中有两个性能问题,分别出于对表达式指数的处理和对表达式的解析;另一个是处理指数函数相关的乘法时出现了问题。
- 对表达式指数的处理不当,导致了我的运行内存问题。我对于表达式指数的处理,一开始是在预处理阶段,直接将其全部展开,例如
(x+1)^2展开为(x+1)*(x+1),导致我在多层嵌套时,会由于展开过长而爆栈。在更改后,面对指数多层嵌套问题,我会先将每层括号内部的表达式化简,再进行展开。 - 表达式的解析是一件比较花时间的事情,在我先前的代码中,存在对于无用的表达式,或者是已经解析过的表达式进行多次解析的情况,从而导致了超出了运行时间。在对代码进行优化之后解决了这一问题。
- 处理指数函数相关的乘法时,由于指数函数相乘可以是括号内的式子直接相加,但是由于我加法的实现需要对相加的式子进行一个初步的解析,而在这两项相加时没有考虑完全,导致加法结果出错。
第三次作业
思路
第三次作业新增加了两个内容,一个是自定义函数可以嵌套调用,一个是新加入了求导因子
- 对于自定义函数的嵌套调用,由于在之前解析函数表达式时考虑了这一点,所以我没有做改变
- 对于求导因子,我主要考虑了两个方面,一个是各个类型因子的求导运算,一个是求导在哪里进行的问题。考虑到统一性,我将求导运算跟自定义函数一样放在了解析时。
具体实现
本次新增加了有关求导运算的类,将预处理给单独拿出来作为了一个类,另外还修改了与求导有关一些类。
- Pre类

- DeriHandle类

- 包括求导运算的全部处理
- 对每个式子递归求导,考虑求导法则的应用
更改的类
- Lexer类新增对求导因子的识别
- Parser类新增对求导因子的解析
基于度量的程序结构分析
- UML类图

- 代码行数

- 可以看到,加入求导操作后,对于原先类的影响都不是很大。新加入的求导操作类也较为复杂。同时由于将预处理操作单拎出来成为一类,Lexer类代码量骤减。
- 类复杂度

- 方法复杂度




bug分析
这次强测没有出现bug,也是这一单元唯一一次。在前面两次作业的缝缝补补下,终究是将漏洞给渐渐补满了。但是互测被hack了一个性能点,在我对代码部分内容进行重构(主要是指数函数的实现)后,解决了这一问题
架构设计体验
这三次作业毫无疑问是一个层层递进,每一次作业都是在前一次作业的基础上迭代开发而来。
- 这三次作业的总体任务可以说是多项式的化简。
- 第一次作业是搭建最基础的框架,其要完成对表达式的解析与基础的运算,这俩个也是整个任务最核心的框架。在此基础上,我的第一次作业还加入了预处理和化简的相关内容。这整体构成了我的第一次作业。
- 第二次作业加入了指数函数与自定义函数,而在第一次作业的架构上,指数函数需要与因子类统一,自定义函数则需要单独处理。基于此,我调整了我的第二次作业架构
- 第三次作业的关键就只有一个求导因子,在前两次作业的基础上,只需要新加入求导相关架构即可。
虽然我认为大体的思路没有问题,但是架构在实现时,我还是觉得有许多不足的地方。
- 最开始我是将预处理放在了Lexer类里面,但其实完全可以将其作为一个全新的类,避免Lexer类负责工作繁杂。
- Factor接口与其实现的几个类之间,有些类没有用到接口所声明的方法,需要调整。
- 计算类Calc现在有些复杂,其包含了加减乘三种运算,在某种程度上来说,加减可以归为一类,乘法归为一类,因此可以对Calc类进行一个拆分,也使得其功能更加明确。
- 对于Term我除了Factor外还单独定义了用来比较两个项是否相等的底数,指数等等,这写应该可以合并统一,但是我还没有考虑清楚具体实现。
现在我们的任务中只有一个自变量x,不存在其他自变量。如果引入新的自变量,如y,z等等,对于我的设计来说,加减法和乘法等的处理只需要在解析时加入对新增自变量的处理即可。但是对于求导因子来说,可能需要重新思考,因为当变量个数较多时,需要考虑偏导数的问题,在这一点上面,我现有的求导运算比较难以处理。
优化策略
本次作业我并没有做太多的优化,只对最后的结果输出做了比较简单的化简处理,比如说去0,去1等等。对于指数函数提取公因式的化简还有指数函数括号的删减我没有付与实现,因为我没有想到一个较好的方法来判断,总感觉会在这些优化上出错,等下次思考充分后,我可能会对优化进行进一步的尝试
心得体会
在第一单元的作业中,我也收获了许多
- 对于面向对象的理解要更加深入了一些。在处理这一单元的作业时,对于对象的管理是一个值得思考的问题,一个是用什么容器来管,一个是关于深浅拷贝的问题,这些都在代码编写中遇到,也需要根据需要来进行选择。还有就是对象间的层次关系,这单元作业中,层次关系是非常重要的一个点,表达式,项,因子等等相互包含,需要我们清楚他们之间的联系,从而更好的协作管理。
- 对于递归下降方法的应用也是这个单元的一大关键,其贯穿于整个单元,从表达式的解析到最后求导因子与之类似的递归处理,都与递归下降方法有关。这也为我们处理需要解析和处理递归结构的问题提供了一个新的方法。
- 完成整个单元的任务也需要编写一定数量的代码,在这期间,对于代码风格的认识也更加清晰。由于有些任务实现较为复杂,如果代码难以理解分析,在找bug以及扩展开发时,很容易迷迷糊糊,不知所云。因此一个良好的代码风格十分重要,包括格式,命名规范,必要的注释,复杂方法的拆分优化等等。
- 这次也有个遗憾,没有编写一个评测机,希望下个单元能够自己搭建一个来帮助自己Debug。
当然,先导课也对于我快速适应这一单元的作业起到了重要作用
- 其让我提取熟悉了Java语言,对java的基础语法,相关操作有了一个初步的认识,写起代码来更加得心应手。
- 也让我提取接触了面向对象思想,能更好的看待多个类及其之间的关系
- 也让我提取适应了oo正课的模式
未来方向
- 感觉互测cost的设置有点小问题,有些复杂数据能够满足cost的要求,但是由于互测评测限制,导致难以通过这些互测点
- 希望OO课程越办越好!!