前言
第一单元的任务的主要内容可以概括为“表达式化简”,通过规定文法,读入一个表达式,化简去括号(当然其中包含识别分析存储化简一系列操作)。
主要的思想即课程组介绍的“递归下降”方法,是一种化繁为简的能够进行层次化处理的方式。
在设计方面,加深了对于面向“对象”编程中的对象的理解。要尽量地将功能按模块分开,并且同时要合适地(考虑到性能和正确性)选取传递的必要的信息,还要避免信息的暴露。
由于第一次作业我着重考虑了正确性的影响,忽略了程序的性能,导致加入了多层嵌套后时间代价激增,故在第二次进行了一次重构。
说明
分析插件使用的是MetricReloaded。
UML图使用BoardMix工具。
第一次作业
第一次作业主要是为了让我们加深对于面向对象的理解,通过所掌握的java知识结合提供的递归下降算法完成一个较为简单的表达式化简。
UML类图
其中数据解析类是的对于字符串解析的类,能够将字符串转化并存储。基础数据存储类里面是第一次进行解析后存储的方式。最终数据存储类是存储最终的一个单项式。
题目简述
本次作业需要完成的任务为:读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号后的表达式。
难点分析
难点主要分为两点。
- 首先是对与输入数据的读取,其中包括化简,递归读取等一系列操作,其中需要结合题目所给的形式化表述将每一种情况准确地判断出来。
当然这可能也不能称之为难点,因为课程组已经将效果不错的递归下降算法给了我们。 - 还有就是化简部分,在进行简单粗暴的解开括号后,要对式子进行尽可能的化简,这其中就包括了对于展开后的式子进行再处理的过程。
本人实现方式
总的来说就是先进行针对符号的初步化简,接着先用简单粗暴的方法直接将括号乘开,组成一个复杂的表达式origin,然后再针对这个复杂的表达式再进行识别后化简至最终形式。
输入预处理
对于表达式中的加减号进行最大程度上的化简。
- 读入的时候即可将两种规定的空白符去除
- 首先是每一个连续的加减号可以进行化简(本次要求中最多可能有三个连续的)
- 其次是表达式(表达式和表达式因子)前面或者所有整数因子的+可以去掉
解析表达式
这里我采用的是课程组提供的递归下降算法,用Lexer进行读取有效字符串传入Parser进行解析,在Parser中进行递归下降准确的读出每个表达式、项、因子并且识别其类别(此处细节比较多)。
括号的拆解(粗暴的)
我直接将拆解的过程写在了toString()方法中
- 在ExprFactor中我是直接将若干个表达式相乘通过多次遍历最终得出这个表达式因子乘开之后样子(即将表达式后^化简)。
- 在Term中我是再先把多个表达式因子相乘(也是暴力版)然后得出一个表达式因子,此时项中只存在至多一个表达式因子和其他因子,再将其他因子乘表达式即可完成括号拆开。
进行化简
此时每个项都只有数字和指数因子,此处化简分为三个部分
- 首先是将所有项本身的数字和指数相乘得出一个“系数*指数因子”的模式,并同时导入TermFinal集
- 再对于TermFinal进行化简,即同类项合并。
- 最后将化简完的结果toString()后,会有连续符号存在,再对于符号进行化简。
输出
输出的时候有几点需要注意
- 为了保证输出是最短的,在有正项的时候要把正项放在第一个舒服并省略加号
- 系数为0的项需要变成0
- 指数因子为1的时候可以省略,为x时可以省略^
所遇bug
进行表达式解析的时候没有考虑足够全的情况以正确读入。
在展开括号内容时循环嵌套错误导致计算错误。
结果为0的时候忘记输出0。
复杂度分析
这里只列出超标的类和类的总复杂度以及均值。
第二次作业
本次作业增加了嵌套括号,自定义函数以及指数函数的要求。难度相对于第一次作业来说看似降低了但是有一些隐藏的问题会出现,由于个人架构问题。
UML类图
本次增加的类主要是在基础数据存储类中的用于存函数的Function和Exponent。由于重构,将最终数据存储类中改为了Poly(多项式)和Mono(单项式)。
难点分析
新增的自定义函数的读取和处理
- exp的相关化简部分
- 多项式相乘带来的内存和运行速度问题
实现方式
这里和第一次作业相似的就不进行赘述,主要是一些改动。
自定义函数的处理
我是讲自定义函数首先进行读取并对于三个参数进行替换为~!@三个符号防止后续出现一系列的替换错误。这里先不做任何处理,直接作为静态变量存在main中。
其次是后续在表达式里面我将自定义函数定义为一种factor,读取的时候先进行替换后再化简作为toString的结果,最后是以ExprFactor的身份存入factors。
exp的相关化简
这里基础的部分只要按照数学中的计算方法进行化简和合并即可。
问题出在去除括号的时候,如果两层括号要进行检验两层括号里是否是factor,如果是则去掉一层括号。
还有就是如果exp的factor是表达式因子,则可以进行提取公因子(num类型)来作为指数进行化简。
但是这两种比较特殊的化简有可能会出现一些问题。
解决爆内存和运行时间过长
这个问题我直接采用的是重构方法,因为我用了两天时间证明了在不改变基本架构的情况下解决不了实质问题。我原来的方法是基于第一次实验的架构,就是通过toString()方法进行拆括号,但这就带来了很大的问题,我每次传的参数都是字符串,具体操作还好说,但是我每次接收到一个字符串后都要进行一系列读取识别操作,非常占用内存和浪费时间。
重构之后我采用了舍友佬的架构,即把每一个类都转化成为一个Poly即多项式,每个多项式由单项式加减构成,单项式Mono仅由三个成分组成,即系数,x的次数,指数函数。
然后每个Poly中用private HashMap> monoMap = new HashMap<>();进行保存,在add和addMono的时候即可在hashmap中实现化简,十分节省时间空间。
遇到的bug
当有多个很长的ExprFactor相乘的时候计算时间代价太大可能会导致RE
exp最后一层去括号的时候偷懒采用未修改的ParserFactor()进行扫描,遇到负号时会直接报错
复杂度分析
第三次作业
第三次作业作为第一单元的任务的最后一次,主要迭代的内容为:
- 新增求导算子 。
- 求导因子可以出现在很多位置,包括函数调用实参,指数函数内部等。
- 为了限制难度,在输入中,求导算子不会在自定义函数中出现,具体见第六部分-数据限制。
- 本次作业函数表达式中支持调用其他“已定义的”函数(保证不会出现递归调用)。
- 本周实验会着重指导求导将如何层次化实现。
本次作业中需要完成的任务为:读入一系列自定义函数的定义以及一个包含幂函数、指数函数、自定义函数调用、求导算子的表达式,输出恒等变形展开所有括号后的表达式。
在本次作业中,展开所有括号的定义是:对原输入表达式 E 做恒等变形,得到新表达式 E'。其中,E' 中不再含有自定义函数,不再含有求导算子,且只包含必要的括号。
UML类图
最后一次作业新增的要求中基于本架构需要增加的部分是基础数据存储类的求导类。
难点分析
本次可以说结合实验的提示实现要求是相当简单,当然这一切都是要建立在前期作业的良好架构下。
- 完成函数的嵌套调用。
- 完成求导功能。
实现方式
这里也主要提及这次迭代中的改动。
首先是函数的嵌套调用问题。这个对于我来说刚好是之前架构的时候考虑了这个问题,仔细思考后选择了在预处理的阶段字符串层面递归调换的方法,刚好能满足本次作业的要求。具体实现思路为:
public static String funDelete(String input) {
String ans = input;
Pattern pattern = Pattern.compile("[fgh]");
Matcher matcher = pattern.matcher(ans);
while (matcher.find()) {
//对这一个函数进行提取和替换
//将替换的字符串替换到ans中
//更新matcher
}
return ans;
}
其次就是求导,我是完全按照实验课提供的思路进行的,进行递归调换,给factor接口增了Poly derive()的方法,在扫描字符串之后存为derive类后转化为多项式。然后在各个类中实现derive()时遵守基本的数学方法,如下:
所遇bug
本次作业因为修改量较少,所以遇到的bug也比较少,主要为两个:
首先是上次作业中定义Poly中add和addPoly()方法返回值问题,这次写作业的时候记反了,导致出现了很多空指针的问题。
其次就是求导过程中Mono求导后会变成Poly,在一开始并未很好的处理。
复杂度分析
这里只列出超标的类和类的总复杂度以及均值。
感想体会
架构是第一位的!
很多时候确实需要多多的集思广益,前人对于相似情况的处理的一些得到总结的方式思想还是很值得借鉴的,如果一切架构都是按照自己构想的,会在有些没考虑到的点上留下很大的问题(就如我一开始只考虑正确性导致后面性能问题很大)。一个好的架构真的能屏蔽一些麻烦,方便程序编写。所以在以后的工程中,一定要先选择一个良好的架构。
还有就是对于SOLID原则中的一些有了切身的体会。
首先是软件组件(函数、类、模块)必须专注于单一的任务(只有单一的职责)。因为当你发现你的组件并没有专注单一的任务时,你就已经开始面向过程编程了,你的程序会滚雪球一样越来越大最后你无法控制。
还有软件设计时必须时刻考虑到(代码)可能的发展(具有扩展性),但是程序的发展必须少的修改已有的代码(对已有的修改封闭)。当你在第一次作业的时候就不能只顾得眼前的一亩三分地,要同时考虑到可能的后续需求能否即插即用。
“只要继承的是同一个接口,程序里任意一个类都可以被其他的类替换,在替换完成后,不需要其他额外的工作程序就能像原来一样运行。”这就是对于某个抽象层次的屏蔽,如我们的factor,我们最后需要做到的就是只调用接口中的方法就能实现想要的效果。这也和“表明一个方法应该遵从依赖于抽象(接口)而不是一个实例(类)的概念”息息相关。
展望
老师曾在课上说这是作业最简单的一次。(还是有难度的)。
其中还是暴露了自己的不少问题,架构的问题前面说了,有一个问题我在这个单元的最终也没能去做,就是试着抛弃文法却能兼容文法。
原因是在和某位同学佬交流时,发现他的程序可以做到很多文法没有规定的事,就比如多变元、少一些没必要的括号等等,确实,真实情况下的数据可能远比文法规定的要变态。
而我的代码是完全按照文法进行编程的,在以后的实践中,要保证自己的代码并非只能适用于某个狭小的环境而能拥有更强的适应能力,是努力的方向。