301
社区成员
发帖
与我相关
我的任务
分享在跌跌撞撞的三周里,OO第一单元的学习圆满结束!感谢上个学期的面向对象先导课,感谢oolens公众号,也感谢努力的自己

这里我借鉴了一位学长的架构,这种架构在讨论区已经有人进行了分享。以下我称之为“基本项架构”。我起初并未理解这一种架构的精妙之处,我是在写代码的过程中一步一步领悟基本项架构的内涵的。
所谓“基本项”,就是构成高楼大厦的一块块砖头,就是我们最终构建出的表达式的最基础的一项。在第一次作业中,这个基本项就是单项式,我们最终想要得到的结果就是单项式的集合。在基本项架构中,我为表达式,项,因子这三个层级都设置了toPoly方法,通过调用toPoly方法自底向上地得到表达式的多项式形式,也就是我们的结果。
在不同层级,toPoly有着不同的实现方式。
我们采用递归下降法。
递归下降法是课程组推荐的方法,我们在开学之前就已经通过OOlens公众号开始学习。在我的解析过程中,也是采用的递归下降法。事实上在第一次作业里,采用正则表达式也能得到正确的表达式解析结果,但是正则表达式无法处理括号的嵌套,而这正是第二次作业新增的内容。在我的理解中,递归下降法和普通的递归有着这样的区别:递归下降法是不断递归调用另一个函数,然后不断向上返回,而普通的递归是不断调用自身,然后从自身返回。从本质来说,递归下降在实现上使用了递归的概念,可以说是一种特殊的递归方法,应用于语法分析。
从这里我们可以看出,在解析时,我们给定一个表达式,自上而下地使用递归下降法得到一个个元素,获得表达式树。然后在计算的过程中我们采用“基本项”架构,利用每一层级的toPoly方法自下而上地得到最终的结果。
在我的第一次作业中,Poly类有两个容器用于管理Mono(单项式),分别是ArrayList和Hashmap,ArrayList存储着Mono,Hashmap是一个Key为指数,Value为系数的Map。后来仔细想了想,实际上只用HashMap存储就行了,其中的Key是单项式的指数,Value是Mono,这样在添加Mono的过程中就直接实现了运算,toString方法只要把HashMap中的Mono调用toString方法然后再输出。
其实也说不上是bug,在第一次作业中我没有在强测和互测中被发现bug,但是在强测中我的性能分没有拿满。在第一次作业中,结果的优化就是合并和正项提前。根据我的想法,是没有做正项的提前导致的。当时交作业比较急,没有仔细想如何做正项的提前,以为是一件比较困难的事。但是实际上按照我的架构来说,只需要在最后输出之前把Mono按照系数大小排序后再输出就实现了正项的提前输出。

以上是部分方法的复杂度(只展示了部分复杂度高的部分方法)
可以看到复杂度最高的是词法分析器部分,此处涉及了大量的if-else用于判断当前读到的字符属于哪一个Token,这里的判断逻辑比较简单,而且只是单层的if判断,实际上出错的概率并不大,所以我就一直没有改,之后在第三次作业中,需要判断的Token变得更多使得这个方法超过60行,我采用之前OOPre中处理各种命令的方法,建立了一个映射用于简化相似的处理,这样代码工作的效率更高了,(但是貌似可读性变差了?)
复杂度次低一点的是Mono(单项式)转字符串的方法,这里是十分容易出错的,涉及到了比较多的化简逻辑:系数为0,为1,指数为0,指数为1……这里也是必须经过多次测试的部分。

这次作业可以说是三次作业中最难的一次作业。
最主要是增加了三个要求:
对于第一个新增要求,只要我们使用的是递归下降法,就能处理括号嵌套的问题
对于第二个新增要求,只需新增一个类就可以,而关键是指数函数的化简和基本项的分析
对于第三个新增要求,也是新增一个类,而函数的解析与调用是这次任务的重点
这次由于新增了指数函数类,对于“基本项架构”,基本项不再是单项式,而是单项式*指数函数的形式,最终得到的表达式是所有基本项的和。
因此在这次作业中,Mono的属性和方法发生了变化。(实际上这里叫Unit更合适,但是我在第一次到第二次作业时懒得改名字了……)Mono的属性由原来的系数和指数变为系数,指数,指数函数的组合,由于Mono的变化,Poly的计算方法也发生了变化。Poly的计算方法应该是这次作业最难写的一个部分。
除此之外,另一个挑战是自定义函数。在这里我借鉴了一位学长的写法,也就是在Definer类里统一管理所有的自定义函数。Definer里有两个HashMap,其中一个HashMap管理函数名和其对应的函数表达式,另一个HashMap管理函数名和形参的对应。Definer中有两个静态方法addFunc和callFunc,不需实例化,直接通过类名就能进行调用。其中的第一个方法addFunc只在Main方法中进行调用,用于在读取函数字符串是对函数进行解析,将函数名和函数表达式(字符串)的对应存储到HashMap中,第二个方法callFunc只在FuncFactor中进行调用,用于得到替换形参和实参之后的表达式字符串,然后再进行解析。(架构图应该还有一条连接Dediner和FuncFactor的线,但是画出来的话太丑了)那么对于FuncFactor来说,属性有newFunc和expr两个,在构造函数中,我们解析利用callFunc得到的表达式,存储到expr中。
对于指数函数的问题,我新增了一个ExpoFactor类。对于指数函数,有两个部分,分别是括号里的因子部分和指数部分。这里为了计算方便,在计算时需要把指数部分乘进括号里,而在最后输出表达式时,为了使表达式尽可能短,需要把括号里的公因子放在指数部分。所以在我的代码架构中,我的ExpoFactor类有三个属性,分别是Factor,exp,Poly,其中Poly是把指数乘进括号里之后得到的多项式,在计算时利用这个Poly进行计算,然后再化简得到的结果。
f(x)=x^2,f((3*x))避免替换成3*x^2exp替换为aaa,然后再进行形参和实参的替换,最后又替换回来,这样就避免了exp和x的冲突我在改完bug提交之后,我又仔细读了一遍我的代码,并没有发现有什么特别大的问题。唯一有需要改进的地方是我几乎把所有的方法都设置成了public方法,而实际上有的方法只是为了减少方法行数而设置的方法,只在自己所在的类里使用一次,这种方法设置成private方法显然更加合适。
在第二次的强测和互测中,我只在强测阶段被hack,包括一个bug和一个性能问题
int形式,因为在这样的情景之下,指数不可能会变得很大。但在第二次作业中,允许括号的嵌套,而表达式的长度仍然有之前的形式,在这样的前提下,指数就有可能超过int范围(这里指幂函数的指数),所以一劳永逸的办法就是把所有的int全部改为BigInteger。exp((2*x))变为exp(x)^2,exp((3*x^2))变为exp(x^2)^3,我之所以没有做提公因式,是因为提公因式不一定会让表达式变短,而只提幂函数因子前面的数字是一定可以让表达式变短的(少了一层括号)。然而,在第二次作业的强测中,出现了两三个点必须提公因式的点,只要做了提公因式的,就几乎可以拿满性能分,而这几个点我的性能分是0
以上是部分方法的复杂度(只展示了部分复杂度高的一部分方法)
我按照圈复杂度的大小排序, 可以看到,复杂度最高的是Mono的toString方法,这个方法需要对Mono的系数,指数进行大量的判断,从而进行输出,因此这个方法中包含了大量的if语句,有的可以使用三目运算符进行一些简化,但是这样的话代码的可读性就下降了。总而言之,这是一个十分复杂且容易出错的代码,需要我们考虑多种情况进行测试。
复杂度比较高的方法还有Poly类的mulPoly方法,也就是Poly的相乘,这里比较复杂的原因是Poly是Mono的集合,Mono又是单项式和指数函数的乘积,进行乘法运算的时候势必要进行遍历,然而我是通过一个嵌套的HashMap来存储Mono的,在遍历一个Poly的时候就是一个二重循环,而整个Poly的相乘就是一个四重循环,这实际是很难简化的。
还有Mono的parse方法,这是专门用来化简指数函数的方法,由于这个方法比较长,我把它单独放在一个方法里进行化简,主要就是之前提到的把幂函数因子前面的数字提到外面,以及加括号的操作(注意形式化表述)
另外一个是Poly类的addMonomial,这里需要比较指数是否相同,比较指数函数的Poly是否相同,如果相同的话如何进行合并……所以这里也涉及到了大量的判断,复杂度高也就在所难免

本次作业新增两个要求
对于第一个新增要求,我们只需要新增一个求导因子类就可以了,然后我们需要为Poly类和Mono类实现derive方法,也就是求导方法。
对于第二个新增要求,在上一次的作业中我使用的是字符串的替换解析函数,直接就实现了对于调用其他函数的解析
主要就是新增了求导因子,新增因子的属性是一个表达式,也就是不管dx内部是什么,我们都把它视为表达式,而真正的求导运算时toPoly方法实现的,在toPoly方法中先把表达式转化为Poly,然后调用Poly的derive方法就实现了求导,而Poly的derive方法又是通过调用Mono的derive方法实现的。
此次我还实现了Lexer的parse方法简化,在前两次作业中,我都是通过一个个的if-else并列来实现字符流的解析,而这次由于新增的求导因子的解析,使得这个方法终于超过60行了,我借鉴讨论区的一位同学的方法,新建了一个类用来存储Map,然后在Lexer中利用这个Map实现解析Token的简化(这里有点像设计模式中的单例模式)
我在这一次作业中吸取上次作业没有化简的教训,实现了提取公因子的操作,而保持我原来的化简逻辑不变。提取公因子放在了Mono方法中,如果检测到指数函数中的表达式前的系数有着公因子,那么就把这个公因子提出来。(只是提公因子的时候出现了bug……)
互测和强测都有点问题
hashCode和equals方法,我只是重写了Poly和Mono类的方法。实际上Poly的equals方法依赖于Mono的equals方法,Mono的equals又依赖于ExpoFactors的equals方法。我在实现Mono的equals的时候没有注意到,导致在某些情况下化简不充分,体现在这次强测中,就是在一个点中我的性能分为0
这里的大部分复杂度高的方法在上一次作业的分析中已经提到了。我们需要注意的是在Lexer中的parse方法,使用了Map映射来替换大量的if-else,确实起到了降低复杂度的作用。
具体每一次的bug在上面已经提到过了,我想在这里说说这些bug产生的原因
第一次作业丢分在性能,主要是我没有做正项的提前,其实是一件很简单的事,但我当时以为很难,脑子没有转过这个弯来,就没有这么做
第二次作业的bug其实我也能避免,问题出在指数位置的类型。我在自己测试的过程中发现了指数函数的指数有可能在化简的过程中超过int的范围,就是指数函数的括号里的数字因子可以很大,提到指数位置上就有可能超过int范围。当时我就已经改了指数类型,但是我没有改变幂函数因子的指数类型,这正是坑点所在……
第三次作业的bug出在指数函数的提公因式化简,实际上这个点在第二次作业已经考察过了,为了提高性能分才选择更加彻底的化简,但实际上第三次作业几乎没有涉及这种化简方式,甚至有几个点提公因式后变得更长了。这样一来二去还不如不化简。
我后来仔细想,这些bug其实是能通过读题,全面测试找出来的。当然,与其后期找bug,更重要的是再写程序的时候脑子要清醒,尽可能考虑到各种情况。
在互测中我发现的bug有大概两种:
exp(-x)的形式是不合法的,我利用这一点成功hack一人在互测过程中,我试过不少找bug的方法,包括测评机,code review等方法,最后发现效率比较高的hack方法还是先找到自己的bug,然后再hack别人。讨论区有人提到攻击性能,我自始至终都没有研究cost,对这种方法并不是特别了解。
我在整个互测中也看到了不同人的不同架构,我向其中的某个人学习了他的提公因式方法,这也算一种收获吧。
在三次作业中,我没有进行过重构,最开始的架构我就十分满意。当然在第三次作业之后发现好像存在性能方面的问题(这个我自己没有测出来,是别人房间的互测数据点,我发现我运行要很久),这个我还没有想到怎么优化。
如果之后还有迭代的话,我的架构也可以应对,比如新增三角函数因子,那么我的基本项就是单项式指数函数三角函数,再处理基本项的求导问题,化简逻辑就是三角函数的各种变换公式,这样就基本完成了一个新的迭代。
第二次和第三次作业的强测点都让我郁闷,第二次作业我没有提公因子,却正好考这个点,第三次我实现了这个点,却又没有考,反而让我的性能分下降了。这些点都是人捏出来的,但在事先却不知道这个点会不会考到,如果为了性能分实现某个化简方式,可能会写出bug,可能会消耗CPU时间,这让我困惑到底要不要实现某个优化,如果优化的话要优化到什么地步。我希望之后的第一单元能说得更加清楚一点。