第一单元包含三次作业。
第一次单纯的解析表达式,输出计算结果,并且不支持括号嵌套。
第二次作业需要支持嵌套括号,并且可以调用自定义函数,支持三角函数。
第三次作业增加一次求导,且函数需要支持调用函数。
三次作业难度评价为2>1>3 ,1,3的排序也许是因为后面逐渐熟练,但也和自己的结构布局的实现有关,接下来便是三次作业的分析。
Homework1
类与方法的度量
类名 | 属性个数 | 方法个数 | 总代码规模(行) |
Append | 0 | 2 | 51 |
Check | 0 | 5 | 98 |
Expr | 2 | 7 | 70 |
Variety | 2 | 4 | 23 |
Lexer | 3 | 4 | 37 |
Main | 0 | 3 | 51 |
Number | 2 | 4 | 25 |
Optimize | 0 | 4 | 156 |
OptLexer | 3 | 5 | 46 |
Parser | 1 | 4 | 84 |
Term | 2 | 8 | 108 |
Factor | 0 | 2 | 8 |
为了便于观察和排版的美观性,每个方法的规模、每个方法的控制分支数目将在类图中给出,三次作业均沿用此方式。
类的内聚和相互耦合情况分析
可以看到,复杂度较高的类是Optimize,在该类中进行了最后的输出处理,将解析后的Expr输出为字符串。内聚性过强,特化了可处理的内容,降低了复用程度。并且对于后面的迭代意义不大,因此后面的作业我更换了架构。
类图
优点:思路清晰,便于维护,可以精确定位到每一步的内容。同时能避免指数符号‘**’对于乘法‘*’的影响。我用9位数字存储xyz的指数,每三位表示一个变量var的指数,便于管理。
缺点:过于固化,各部分功能难以支持更多的扩展,后面需要重构。
Homework2
类与方法的度量
类名 | 属性个数 | 方法个数 | 总代码规模(行) |
Cos | 3 | 4 | 42 |
Sin | 3 | 4 | 42 |
Single | 5 | 15 | 254 |
Parser | 1 | 4 | 98 |
Expr | 3 | 5 | 52 |
Variety | 3 | 5 | 59 |
Lexer | 3 | 7 | 85 |
Main | 0 | 2 | 60 |
Num | 1 | 5 | 32 |
MakeFunc | 0 | 5 | 145 |
Promote | 0 | 4 | 32 |
Poly | 1 | 5 | 104 |
Term | 1 | 2 | 22 |
Factor | 0 | 3 | 9 |
与第一次作业相比,增加了两个类sin和cos,删除并更换了几个类。原因便是结构和思路都发生了变化,具体内容会在下文架构设计处解释。
类的内聚和相互耦合情况分析
这里只展示了复杂度较高的部分,集中在对sin和cos的处理上。sin(Factor)中可以嵌套表达式,因此在存储和判断上较为复杂,如果要判断两个sin相等,就需要遍历它们的子表达式,这样会极大的增大开销,因此我以字符串的形式存储sin和cos的内容,便于比较和遍历。
类图
优点:使用两次递归下降,可扩展性较好,只需要使新因子实现Factor即可,同时将指数的信息保存在Factor中,在多项式处理时再展开,可以处理更大的指数,更多的连乘,降低了开销。
缺点:递归较多,debug查询比较麻烦。对于内存开销比较大。
Homework3
类与方法的度量
类名 | 属性个数 | 方法个数 | 总代码规模(行) |
Cos | 3 | 4 | 63 |
Sin | 3 | 4 | 62 |
Single | 5 | 15 | 254 |
Parser | 1 | 4 | 98 |
Expr | 3 | 5 | 112 |
Variety | 3 | 5 | 77 |
Lexer | 3 | 7 | 85 |
Main | 0 | 2 | 86 |
Num | 1 | 5 | 50 |
MakeFunc | 0 | 5 | 145 |
Promote | 0 | 4 | 32 |
Poly | 1 | 5 | 104 |
Term | 1 | 2 | 110 |
Factor | 0 | 3 | 15 |
DeParser | 0 | 4 | 137 |
Derivation | 0 | 3 | 63 |
类的内聚和相互耦合情况分析
在作业二的基础上实现了一个求导功能,该功能复杂度较高,但符合规律。能够支持常数,变量,表达式,三角函数,自定义函数的求导。
类图
优点:作业二的架构基础上增添了求导模块,不会影响原功能,并且可以直接删去该模块而不影响原功能
缺点:在求导模块的优化不够,如果遇到极大规模的数据,可能运行时长会很长,在这里也错了一个点,原因是time limit exceed
架构设计体验
三次作业的架构理论上应层层递进,迭代开发,但是第二次作业的跨度挺大的,支持嵌套,支持自定义函数,支持三角函数。我第一次的架构通过类图可以看出,在递归下降解析完表达式后,是通过数字的不同位来输出字符串的,比如 001 020 003 输出 x*y**20*z**3,这个构造方式在作业二中具有极大劣势,难以表达sin和cos内部的因子,如果内部因子过大,就很可能出现各种错误。
因此我也通过这个机会进行彻底的重构,恰好上了一节研讨课,同学们给予了我启发,我使用多项式和单项式的方法来消除指数和表达式因子相乘的问题,在递归过程中将相同项合并,存储在最终的poly中,这个架构的优势在于他是逐层进行的,不需要最后的综合。可以直接输出sin内部的因子成为字符串,在sin中储存一个字符串即可。
第三次作业沿用了第二次的架构,通过类图可以看出,只是多了两个新类,并且对factor的实现类增加了几个方法。遍历一边输入,将求导因子提前运算,返回完成后的表达式。这样就回到了第二次作业的情况,直接调用方法即可。
总的来说,重构不一定是坏事,一个良好的架构能省去很多后续的工作,并且越早发现自己的不足越好。在结构上可以将功能模块化,使不同功能的耦合度降低,达到拆去模块不影响工作的效果是最好的。
程序bug分析
作业1:
强测没有错,互测错误在于数字位数,我错误的估计了评测机的代价要求,最开始用6位数即00 00 00 来存储xyz的指数,上限99,结果同学使用一个y**104的结果帮我查出了这个漏洞。
该出错方法行数和圈复杂度并不高,是我设计时的错误,改为000 000 000 就能解决问题。
作业2:
强测错在 形如-(x+y)**0 的表达式上,没有输出负号,互测测出一个 sin(0)**0 ,应该得1 却输出了0,经检查,问题均出在指数为0的 表达式的判断上,没有特判负号和表达式为0的情况,导致了错误输出。
该方法复杂度较高,由于对于指数0的判定写在了方法的最后,导致了遗漏。
作业3:
互测没错,强测一个点超时,原因是对于一个极大数据求导,会产生极多项,比如 dx( x*1*1*1),会输出4项,1*1*1*1,x*0*1*1,x*1*0*1,x*1*1*0,我没有处理乘积为0的项,而这些项在返回时又会被拆分为一个个表达式,导致极大的增加了不必要的计算开销,造成了超时,后来增加一步判定便解决了问题。
该方法复杂度较高,问题也在于设计时未考虑过大数据的开销问题,以后对于这种临界问题要多加注意。
发现他人bug
每次写作业时,我会顺手记录在思考过程中想到的特别样例,以及调试自己程序时发现的错误点,同时通过阅读测试对象的代码来思考可能的错误。
三次互测都有成功发现他人的错误,大部分错误是通过 特别样例 查找到的,原因可能是在构思阶段思考的比较多,会设想一些特别的样例来测试自己的程序,如果有同学思考的不全面就可能出错。其他错误便来自精准代码打击。作业很容易在格式判断方面,特殊情况上找到错误,一般查看最多的便是数据预处理和字符串的解析,结构上很少会错,要不然也过不了中测。
心得体会
实话实说,对于自己这一单元的成绩还是知足的,(虽然就差一点点就能更好ㄒoㄒ)。我在上学期没能抽上先导课,自己在寒假学习了Java语法。但是还是在第一周被打了个措手不及,只会语法而在结构设计上一窍不通,自己查找资料学习,向同学询问,苦熬三天完成了第一次作业。一出来强测全过,当时是十分高兴的,证明自己的努力没有白费。第二周的作业一出来,发现自己需要彻底重构,我吸取同学们的经验,加上自己的理解,又是大战三天取得最终战果,当然也是累得不行。不过第三周就好多了,有了清晰的思路,一个下午就完成了迭代。
这一个月的体验深刻地告诉我,人在认真的时候是真的能挑战自我,从无到有的完成一项艰巨任务是很有成就感的事情。唯一的遗憾就是那几个测不出来的错误点,不过人要知足常乐,分数平均下来还是很不错的,下一单元要更加努力。
感谢努力的自己,感谢同学,更要感谢助教和老师们的帮助。