2021北航oo第一单元(表达式求导)总结
2021北航oo第一单元(表达式求导)总结
1:第一次作业
1.1:题目分析
第一次作业为简单多项式导函数的求解。多项式是由加法和减法运算符连接若干项组成。项由乘法运算符连接若干因子组成。因子分为常数因子和变量因子(幂函数)。
1.2:实现方案
第一次作业我建立了Addderi和Multideri两个类,Addderi类负责将输入的字符串按+、-号分开存放在一个ArrayList中,Multideri类负责将该ArrayList中的元素按*分开,判断是常数因子还是变量因子并返回求导结果。
为了顺利分离出因子,在MainClass中我进行了去空格、合并连续的加减号操作,在Addderi中进行了将**替换为 !,表正的+替换为@,表负的-替换为&的操作,在Multideri中进行了将@换回+,&换回-的操作。
程序UML图如下:

1.3:度量分析
从中可以看出第一次作业Addderi类中的deri方法基本复杂度过高,应当予以简化。模块之间的调用关系也很复杂,模块耦合度很高,主要原因在于第一次作业采用了面向过程的方法,在一种方法里反复调用了另一个模块。总体来说,整个程序的圈复杂度很高,代码难于测试和改进。
1.4:BUG分析
第一次作业的公测没有被找出BUG,但互测被人暴捶。究其原因是因为没有考虑三个连续加减号的情况(我以为只有开头才会出现这种情况,这三十多个同质BUG搞得我心慌慌)。不过好在这个BUG很好修复,只要把连续合并两次两个加减号在一起的情况就好啦。
第一次查别人的BUG时很尴尬,手里没有评测机又来不及写,只是把自测时出错的BUG挨个给他们测了一遍,自然是什么都没测出来。不得不说大佬们考虑问题确实很全面,三个连续加减号这个事情说的并不是很明白,我房间里的人的代码里居然都做了相应的处理。看来得尽快搞个评测机了呢。
2:第二次作业
2.1:题目分析
第二次作业相比于第一次作业新增了三角函数因子和表达式因子,本次作业三角函数因子只包括sin(x)和cos(x)两种,表达式因子由一对小括号及其包裹的表达式组成。
2.2:实现方案
由于第二次作业包含了表达式的嵌套,我感觉使用第一次作业那种完全面向过程的方法会非常麻烦,所以果断进行了代码重构。
定义了接口类Factor,因子类ConstFactor、Cosfactor、ExprFactor、PowerFactor、SinFactor表达式类Expression, 项类Term,工厂类Factory。
第一步是对输入字符串进行拆分,这一步用到了递归下降的方法:
-
将字符串在Expression类中按最外层的加减号拆开(这里的最外层通过括号匹配实现,只有在遇到加减号且左括号与右括号匹配时才拆分),将得到的字符串依次传入Term类。
-
将字符串在Term类中按最外层的*拆开(同样进行括号匹配),将得到的字符串依次传入工厂中进行正则表达式判断,并返回对应的因子类,若是ExprFactor类则会将该字符串反传回Express类再进行新一轮的拆分。
-
将整个字符串都拆分成ConstFactor、Cosfactor、PowerFactor、SinFactor因子类后逐层返回,最终得到一个被完全拆开的Expression类。
第二步是对字符串进行求导:
-
在接口类Factor中定义了diff方法,其他所有类都继承了这一方法。Expression类中实现加法求导,Term类中实现乘法的求导,SinFactor类中直接输出cos(x),Cosfactor类中直接输出-1*sin(x),ConstFactor类中直接输出0。
-
将每个因子的求导结果逐级上传,进行字符串合并,最终得到求导的结果。
程序UML图如下:
2.3:度量分析
从中可以看出MainClass中进行的字符串预处理以及表达式和项进行拆分和求导前的处理过于冗长,导致代码复杂度一下子高了许多,应该把这一部分单独写成一个函数。但在拆分和求导过程中,工厂模式的使用确实大幅降低了各个类之间的耦合度,工厂及其对应的各个因子类的基本复杂度和和模块设计复杂度都很低。
2.4:BUG分析
由于整个代码都进行了重构,出现的问题真的是数不胜数,下面我将出现的BUG分为自测、 中测、强测和互测四个部分。
- 自测
自测中出现最大的问题是由于递归的存在导致乘法求导出错,错把乘号前面表达式求导的结果再次求导了,导致进入无限循环当中,当时还一直以为是拆分的细节除了毛病,找了几个小时都没找到,结果是连根源都错了。
自测中出现的另一个问题是是表正负的加减号的问题,由于第一次作业中不存在表达式的嵌套,所以我直接对刚输入的字符串进行了负号的替换,但是在第二次作业中,如果我只是单纯地把正负号换掉再换回来,在生成表达式类的时候会出现空指针,求导过程中会报错。所以我将表示正负的加减号替换成了+1 *和-1 *,看做项进行处理,解决了这个问题。
- 中测
中测中我的括号匹配机制除了问题,我的判断机制中有一步去最外层括号的操作,但我却没有考虑到最外层有多个同一级括号的情况,把这种形式的也给去括号了,导致出错。
- 强测和互测
很不幸,强测和互测这次出了好几个TLE的错误,这个事情确实让人有些无从下手,其中有一个数据是一大堆括号套着的一大堆x的连乘,这种东西都能搞出来我实在是佩服,用户的脑洞属实是程序员们难以想象的呀!最后我尝试合并了几个循环,但只能AC其中的一个点。
这一次的互测为了防止被屠杀却毫无还手之力,把学长传下来的对拍器修改了样例生成条件和判定条件直接使用了,发现房间里一个人的代码在三个加减号后面跟一的情况会出错。
3:第三次作业
3.1:题目分析
第三次作业相比于第二次作业增加了三角函数内的嵌套因子和表达式的正确性判断。
3.2:实现方案
首先是三角函数内嵌套因子部分。因为第二次重构的方法很成功,这一部分只需要改动Cosfactor和SinFactor内的diff()方法即可,在这里我用了可能比较取巧的方法,即没有改动拆分字符串的函数,而是在求导过程中对sin()或cos()括号内的因子进行解析。
其次是表达式正确性判断部分。我将表达式的正确性判断分为了两个部分:第一部分是在MainClass中,如果说在输入时胡乱输入的话,我的程序抛出空指针异常或是数组越界异常(虽然我把所有的异常种类全都写上去了),用try、catch捕获即可。第二部分是一个单独的CheckOut类,用于对原封不动的输入字符串进行格式正确性判断,其中包含了我能想到的所有错误情况(指不是乱输的那种),如:数字中间有空格、表正负加减与数字间有空格、连续加减号数量超标、三角函数里面不是因子、存在表达式因子的幂等。
程序UML图如下:
3.3:度量分析
好吧不得不说,这种检查表达式正确性的方法不仅本身复杂度很高,也导致了代码耦合度的提高。其他方面因为没有大的改动,所以与第二次作业差不多。目前对于这种情况,我想到了两种解决方案, 第一种是把CheckOut中的种种判断分别写成单独的方法,另一种去掉CheckOut,在每一个类里对输入的字符串进行对应该类的正确性检查。
3.4:BUG分析
本以为这次只是小改动,不会出太多的BUG,结果还是事与愿违。先是出现了好几种没有考虑到的错误情况,导致错误格式也给出了结果。然后就是一个困扰了我两天的离奇测试点:中测3:这个点在讨论
区、微信群以及qq群都卡了无数人,我眼睁睁地看着他们一个个地解决,眼睁睁地看着他们找出BUG的样例我都是对的,实在是心里苦呀。在检查了无数遍代码、用了一大堆讨论区的测试数据,甚至用评测机查了一百多个样例、正准备放弃中测的时候,我室友随手敲了个sin(cos(-1)),输出结果的格式居然是错的。我这才想到我只测了三角函数最外层括号里是一个复数的情况,却没想到还可以嵌套一个三角函数在包含一个符号,导致把-1拆成-1*1后出现格式错误。
强测被查出了一个小BUG,sin(+ 030)会判定为正确格式,我就直接在CheckOut里加入了"\([+-][ \t] "正则表达式的判断条件。
互测中奇迹般地没有出现BUG,毕竟第二次互测改过一次BUG了,这一次的BUG 不再那么好找。
4:第一单元作业感想
以前从来没有接触过java,更没听说过面向对象的设计方法。但现在回头看看第一次作业的代码,真真是惨不忍睹。如果真的把第一次作业这种完全面向过程的做法应用到第三次作业当中,我恐怕是能把自己写懵。
说一些我目前对于面向对象的理解吧:首先万物都可抽象成一个类,程序中出现的每一个概念都可以抽象成一个类,这个类分的要尽可能细,可能这样会导致程序的规划很麻烦,但此后加功能时会变得很
容易。其次是工厂模式真的好用,用一个工厂类在那里,写的人方便,看的人也方便,程序脉络更加清晰。继承也是面向对象必不可少的一部分,以前写c语言程序的时候总是搞一堆强制转换,转来转去还是错的,继承完美解决了不知道输入类型的问题,所有子类都可以调用父类的方法。
至于递归下降这个方法,是在我写完了整个单元之后,才知道原来这种方法就是递归下降,其实递归下降说白了,就是一层一层地拆,拆到最小的因子再返回,这种方法简单且直接,个人认为比写大量的正则表达式好很多,debug也要容易不少(我也实在是写不出来大正则表达式)。
浙公网安备 33010602011771号