BUAA-OO Unit1 表达式求导
1 历次作业分析
1.1 第一次作业
1.1.1 设定的形式化表述
表达式 → 空白项 [加减 空白项] 项 空白项 | 表达式 加减 空白项 项 空白项
项 → [加减 空白项] 因子 | 项 空白项 * 空白项 因子
因子 → 变量因子 | 常数因子
变量因子 → 幂函数
常数因子 → 带符号的整数
幂函数 → x [空白项 指数]
指数 → ** 空白项 带符号的整数
带符号的整数 → [加减] 允许前导零的整数
允许前导零的整数 → (0|1|2|…|9){0|1|2|…|9}
空白项 → {空白字符}
空白字符 →
`(空格) |\t`加减 → + | -
其中{}表示0个、1个或多个,[]表示0个或1个,|表示多个之中选择。若输入字符串能够由“表达式”推导得出,则输入字符串合法。
1.1.2 实现方法
由于本次作业没有格式检查,我在获取输入字符串后将所有空格去掉、多个加减号合并为一个加减号,并在没有符号的首项前面增添一个+号,以方便处理。
由于没有三角函数、表达式因子和括号的嵌套,因此我选用了使用正则表达式匹配每一项的方式,匹配到的项为term类,每读到一个项就将输入中对应部分删除,如此循环往复,直到所有项都被读入。
在项内,当系数为1时,可以省略为+x和-x,将其还原为+1*x和-1*x,以统一格式(其实放到factor内完成可能更好?)。将**转化为^,并通过*作为分隔符使用split分割为因子。项分割得到的因子factor包含系数coe和指数exp两个变量,如果为常数,则exp=0。事实上,在本次作业中,factor只有幂函数一种形式,所以每一个term都可以化简为一个等值的factor,因此我采取先进行项内合并的方式,即各因子系数相乘、指数相加,求得化简后的term,再将所得到的factor进行求导。
对于项的存储,我采用了第一次试验中TreeMap的形式,key值为项的指数,value为factor。在求导完成后,遍历TreeMap,根据TreeMap中是否存在key为该指数的factor确定是否需要进行合并同类项。
在完成求导后,调用Term类的toString方法,遍历Treemap对其中的每一个factor调用toString方法输出结果。
1.1.3 架构分析
类图:

在main函数中通过正则匹配获取一个项的字符串,创建Term类的一个实例,传入getterm方法中,在getterm中进行分割、化简得到Factor,调用diff函数返回求导完成的Factor,并将其存入Map。所有项求导结束后,调用Term.toString,遍历Map对其中存储的Factor调用toString方法。
度量分析:

ev(G) 为基本复杂度,iv(G) 为模块设计复杂度,v(G) 为圈复杂度。可以看出,Factor类里的toString方法复杂度最高。由于写这部分内容时头脑比较混乱,使用了大量的if-else结构。后来发现测试中错误的点也大部分出现在toString方法里,例如输出0、多输出符号的问题。
getterm这个方法也是一个大杂烩,其中完成了化简项、因子求导、合并同类项和存储这三个功能。在表达式相对简单的情况下,方法还不算太复杂,但后续架构沿用了类似的想法,使得方法变得非常冗长。
1.1.4 bug分析
本次作业的bug主要有两个,一个是在应该输出0的时候没有输出,源自于toString方法构造的缺陷;另一个是在合并项内因子、初始化coe的时候初始化为0,事实上应该初始化为1。
1.1.5 hack策略
观察了房间里同学的代码,由于同学们大多数采用正则表达式匹配,因此构造了多个+-号与空格的样例;针对同学没有使用biginteger构造了更长的数据。
1.2 第二次作业
1.2.1 设定
相比于第一次作业,增加了
三角函数
sin(x)或cos(x)(在本次作业中,括号内仅为x)
一般形式 类似于幂函数,由
sin(x)或cos(x)、指数符号**和指数组成,指数为一个带符号整数,如:sin(x) ** +2。省略形式 当指数为1时,可省略指数符号
**和指数,如:sin(x)表达式因子 由一对小括号及其包裹的表达式组成,如:
(x**2 + 2*x)。表达式的定义将在表达式的相关设定中进行详细介绍。
1.2.2 实现方法
本次作业完成得非常艰难,起初计划在上一次的方法上做出进一步扩展,仍然使用正则匹配项,将一个项分为常数系数、x、sin、cos,并用term类中的四个变量分别记录系数、x的指数、sin的指数、cos的指数,在求导过程中不论是否存在x/sin/cos,一律提出x/sin/cos的指数和系数对其求导,每个项共需要求导三次,最后将其放入map。但实际完成中,发现正则表达式匹配括号嵌套是一个非常痛苦的过程(后来知道JAVA的正则不支持递归),并且由于表达式因子的存在,如果不拆开括号无法化简项。在周五研讨课上听到了同学关于递归下降的实现的想法,于是当天晚上开始重构。
总体思路如下图:

读取获得多项式Poly,使用不在括号内的+-作为分隔符将其分割为Term,再使用不在括号内的*作为分隔符将其分割为Factor,最终对factor求导。Factor有三个子类,其中Power代表幂函数和常数(即cos = xx, exp = 0 的幂函数),Triangle代表三角函数,将sin和cos存储在label内,Expr代表表达式因子,这里以一个字符串的形式储存;在对表达式因子进行求导时,去掉最外层括号,得到一个多项式poly,再对其进行递归求导。
由于求导时需要使用到链式求导,在Term类中设置容器贮存所有Factor。
在求导过程中,Power和Triangle求导返回Factor的集合(主要考虑Triangle指数不为1时,求导产生Factor的乘积),Term求导返回Term的集合,Poly求导最终也返回Term的集合。
由于时间紧迫,没有进行优化。事实上直到下一周才想通并正确完成了Poly→Term→Factor的分割,采取了依次读取字符,类似于有限状态机的方法。并且实现得相当不雅观,结构类似的函数反复地在不同类里出现。
1.2.3 架构分析
类图:

不知道为什么没有create关系
度量分析:

其中复杂度较高的几个方法:

除去toString方法,项和多项式的diff方法复杂度也很高。我在diff方法里做了过多的工作,特别是Term.diff,其中包括了字符串的预处理、term分割为factor字符串,再通过字符串生成Factor存储在容器中,最后对容器中的factor依次求导,总长度达到100行以上。其实,Factor的生成应该可以通过工厂模式完成,其余的工作也应该分别交给不同的方法去处理。
1.2.4 bug分析
本次的bug主要出现在生成factor的过程中,由于本次括号可以嵌套,还引入了三角函数,无法再像之前那样根据^的位置就可以提取指数(例如sin((x**2))**2)。程序向exp中赋了奇怪的字符,导致bug。同样引入有限自动机的方式处理,可以改正bug。
1.3 第三次作业
1.3.1 设定
相比于第二次作业,增加了
本次作业允许将任意因子嵌套至三角函数中,并将嵌套后的整体作为一个因子。
要求进行格式检查。
1.3.2 实现方法
本次求导工作基于第二次作业的基础上,只需修改Triangle类使其在括号内包括一个Factor,对Factor再进行一次求导并将求导结果加入求导得到的Factor集合即可。
格式检查的实现并不尽如人意,我在之前的工作中都采用了先去掉全部空格、合并加减号再处理的方法,没有为格式检查做好准备,在这次作业中使用了一些正则匹配的特判,如果匹配到这些错误格式就输出WRONG FORMAT(例如* * 、2x等等),完成特判后进行字符串的预处理;之后在生成factor的过程中和factor内进行格式检查。但这样做很难覆盖全部错误格式。现在看来,应该将格式检查的过程分散到递归下降解析表达式的过程中,在完成分割的同时完成格式检查。
在对Factor求导返回Factor集合、将Factor集合加入Term的过程中,遍历容器进行了幂函数和三角函数的简单合并同类项。
1.3.3 架构分析
类图与第二次作业大致相似,不再放出。
度量分析:

由于时间原因没有进行大的变动,格式检查让diff方法变得更复杂了(。
1.3.4 bug分析
本次强测遇到的bug都是出在格式检查中,一是忘记了判断表达式因子的幂运算是不合法的,二是程序在解析左右括号不相等的表达式时会出错,三是带符号整数本身的符号与整数之间也不允许包含空白字符的问题。前两个错误很好解决,第三个错误如果修改为递归下降的格式检查应该能比较方便地处理吧?
1.3.5 hack策略
没有hack到。
2 重构经历总结
在第一次作业的时候,完全没有参考也没有多做了解,按照面向过程的习惯草草写了两个类,难以进行后续的迭代,所以第二次作业选择了重构。重构时,从研讨课上已经大致了解了递归下降的思路,但具体实现起来还是遇到诸多问题。现在回想起来,这次重构一是时间太短,到最后半天脑子里已经一团乱麻,效率很低;二是只想好了一个大概的思路就开始写代码,在写的过程中才一步步发现了很多问题,再去改之前的代码,这样导致思路越来越乱。所以,在开始落实思路之前,应该留下时间考虑好实现过程的每一个细节,再开始动笔。
3 心得体会
OO第一单元的学习结束,首要感觉是压力比较大,如果在半途决定重构,时间会比较紧张;如果在重构过程中又遇到一些比较严重的纰漏,会给人不少压力。其实现在回想起来,第二次作业是完全可以在规定时间内完成的,但是由于连续工作很久进展又比较缓慢,最后的一段时间工作效率几乎为0。当然,与其提高所谓抗压能力,不如在第一次作业就整理好大致的架构,为后续扩展作准备,尽量避免重构。
第二是深刻地意识到自己仍然不能很好地以面向对象的思路去分析问题,对于继承、接口也一知半解,这会导致设计架构时的困难、相近代码的复用、并且让人写出一些看起来非常愚蠢的代码。

浙公网安备 33010602011771号