301
社区成员
发帖
与我相关
我的任务
分享在此次的程序开发中,我选择了递归下降的策略,这是一个较为普遍采纳的框架。此框架核心理念在于对输入的表达式进行抽象分层,细分为表达式、项以及因子三个级别。具体来说,表达式是由项构建的,项又是由因子构成。在具体的处理过程中,采用递归的方法逐级深入,直至因子级别,依据需要处理的因子类型采取相应的解析策略,并在解析完成后返回该因子,至此一次递归过程完毕。
这种架构相比直接对字符串进行解析,在多个方面都显示出显著的优势。首先,它具备较高的容错性和可读性,每一个层级的行为都清晰可见,出错时也能较容易定位到问题所在的具体层级和方法。此外,这种框架不容易引入细节错误,能够有效规避许多难以通过常规测试发现的小问题(例如,在字符串解析中需要细致考虑符号顺序等复杂细节,而递归下降方式能够自动处理许多此类问题)。其次,代码的可扩展性极强,对于大多数新增需求,通常可以通过增添新的因子类型来应对。对于类似嵌套括号这样的问题,甚至无需额外处理,因为该架构已经自然包含了对嵌套结构的处理能力。

如上类图所示,由移位器Lexer和解析器Parser组成解释模块,将表达式抽象为Expr、Term和Factor三个层次进行解析。设定Factor接口,Expr(表达式因子)、Term(项因子)、Var(变量因子)和Num(常数因子)四个类实现了该接口。
优点:采用Factor接口,让不同类型的因子去实现Factor接口,使得代码更加灵活,易于扩展。采用递归下降的方法,使得代码结构清晰,易于理解。
缺点:在处理表达式的过程中,尤其是Term类的appendFactor方法,未能有效的合并同类项,为爆TLE埋下了隐患(可惜当时没注重)。



在进行方法复杂度的评估时,发现Expr类中的consolidateExpression和print方法,以及Term类中的appendFactor方法,复杂度较为显著。这一现象主要归因于这些方法承担了过多的功能,且未能有效地将部分功能独立为单独的方法,从而使得这些方法显得过于庞大。
在对类的复杂度进行分析时,明显地,Expr、Parser以及Term类的复杂度较高。此现象的主要原因是这些类包含了较多的方法。
hw1要实现的功能比较简单,于是笔者在优化输出长度上多做了些工作,先是去除冗余的加减号,再是对于同类项进行了合并(本次作业的同类项还是比较好判断的),甚至还对开头尽量不要放负号进行了优化。不过大家的优化策略应该都差不多,最终也拿满了性能分。
总体来说优化部分的代码还算是较为简洁,除了关于开头负号的优化实现的有些不够简洁。
hw1需要实现的功能较为简单,强测和互测都没有出现bug。
在hw1的基础上,hw2引入了对嵌套括号、自定义函数以及指数函数的支持,是一个相对复杂的迭代过程。如同之前所述,递归下降架构展现出了其出色的可扩展能力,在本次迭代中尤为明显。首先,对嵌套括号的支持已经在第一次作业的框架中被顺利实现。接着,对于自定义函数和指数函数这两种新的因子类型,实际上我们所需做的,仅仅是增加两个实现了Factor接口的新类。
然而实际操作过程却充满挑战。在实现自定义函数的过程中,联想到OOpre中Shop,笔者用单例模式创建了FunctionLib用于存储自定义函数,并引入了Function类来负责自定义函数定义式的解析及处理。当解析表达式遇到自定义函数时,程序会直接对经过自定义函数处理的表达式进行解析,这实质上是一种新的表达式解析方案。在此方案中,遇到相关变量时,会将传入参数作为因子返回,并在处理完毕后,再将整个表达式作为一个表达式因子返回。至于指数函数的处理,则相对简单,只需将指数函数括号内的部分作为一个表达式解析,然后附加上指数函数名称即可。

通过分析类图,课件本次作业的结构相较于之前有了显著的复杂度增加,这主要归因于引入了两种新的因子。对于Expr类中方法数量的增加,则是因为在对初次作业的架构进行优化的过程中,我将部分原始代码重构为新的方法,这不仅增强了代码的易读性,也提升了其维护性。
优点:采用了单例模式,使得FunctionLib类只有一个实例,方便了对自定义函数的管理。
缺点:在处理表达式的过程中,尤其是Term类的appendFactor方法,未能有效的合并同类项,在强测中惨爆TLE。

由图所示,代码总行数达到748行,远超第一次作业,可见本次作业迭代量还是很大的,所幸在完成第一次作业时预留了较好的扩展性,使得本次迭代的工作量不至于很重甚至重构。


如图所示,虽然本次作业还有一些方法复杂度较高,但相较于第一次作业,这些方法的复杂度已经有所降低。这主要归因于在第一次作业的基础上,我对代码进行了一定程度的重构,将部分功能独立为单独的方法,使得代码更加清晰易读。(不过hw2的任务量还是不小的,更多时间用于实现功能了,在复杂度优化上还有提升空间)

如图所示,新增的自定义函数类Function也有较高的复杂度,这是由于该方法在实现解析函数表达式的基础上,还实现了函数参数的处理以及函数调用等方法。
hw2中增加的优化就比较少了,主要功夫都花在功能实现和保证正确性上。另外对于括号进行了一点优化,对于括号内因子不为表达式因子的可以优化掉一层括号。不过笔者在判断因子是否是表达式因子时大意了写错了一行逻辑,导致对有的情况多优化了一层括号并在强测丢分,还是有点可惜。总之优化的比较简洁但不够彻底,甚至没有保证正确性(悲)。
尽管如上所述构想的很好,但本次作业还是出现了比较惨痛的bug,被分进了C房,互测也惨遭hack。
本次作业出现了两个bug:
第一个bug造成原因是没看清题目要求进行了过度优化,对于exp((-x))这样的结果输出成了exp(-x),多贪了一层括号。这个bug并不难发现,完全是没有充分测试造成的,在bug修复阶段进行了微调解决了此问题。
第二个bug则较为棘手,出现了TLE。这是因为在parse过程中,程序在Term类调用appendFactor方法时,需将新加入的因子与原有的各因子列表合并,即遍历新加入因子的各个因子(如果是表达式因子的话),再遍历已有的因子,两两相乘,最终汇总得到新的因子列表;而如果出现乘方,则会重复这个过程。然而这个过程中并没有进行合并同类项化简,例如处理(((((((((((x^8)^8)^8)^8)^8)^8)^8)^8)^8)^8)^8)^8,就会导致有大量因子的未化简未合并因子堆积在一个Term中,从而导致了TLE。笔者一开始设想在appendFactor方法中加入合并同类项的逻辑,但发现若每次执行appendFactor方法时都对Term中的因子进行合并,仍会导致大量的重复计算,从而仍然使得程序运行时间过长。并且为了合并同类项还需要对基本因子添加指数项,处理起来很复杂,并且在hw3加入求导因子后更加难以实现更复杂的功能。
因此笔者决定对整个代码进行重构以解决该问题,改进后的框架见下文的hw3。
第三次作业进行了重构,在parse阶段将所给表达式用递归下降法化为后缀表达式。为了便于化为后缀表达式,在原有的Expr-Term-Factor的层级结构基础上,在Term和Factor间增加了一个Powfun层,专门用于记录幂函数的后缀表示,该类转换为后缀表达式时只需将内部因子toString后再加上" ^"。
之后用栈计算后缀表达式,采用单项式类Expo和多项式类Item存储计算过程中的单项式和多项式。边计算边合并同类项,最后将多项式类Item转化为字符串输出。这样就避免了hw2中的TLE问题,不会在parse过程中造成factor的堆积,而是转化为后缀表达式之后再统一进行计算化简,大大降低了时间复杂度。
单项式类Expo中,存储x的指数expoX,再用一个HashMap存储其中每个exp项的指数,例如可表示x^2*exp(x^3)^2。
多项式类Item中,用HashMap存储多项式中含有的单项式即对应的系数,即$HashMap<Expo, BigInteger>$。同时多项式类中实现了多项式的加法、减法、乘法、乘方、求导等功能。
对于新增的求导因子的功能,同样将dx操作视为一个运算符在parse阶段解析为后缀表达式,增加了求导方法类Derivative。其中derivativeFunction方法用于实现单项式的求导,termDerivative实现对某一项的求导,采用链式法则即可得到其求导后的结果。
这样的架构设计有较强的可扩展性,例如面对迭代场景:若要增加对积分的支持,只需将积分也视为一种运算符,加入后缀表达式中,再增加一个Integrate类实现相应的积分操作即可。
另外,hw3还要求支持自定义函数支持调用其他自定义函数,该功能笔者通过SelfFunc类中的subFun功能进行递归替换进行实现。

由类图可见,本次作业进行了较大规模的重构,并同步修复了hw2的TLE问题。采用了Expo-Item的单项式-多项式结构,并创建Derivative类实现了求导功能。在parse阶段,将表达式转化为后缀表达式,再用栈计算后缀表达式,最后将多项式类Item转化为字符串输出。
优点:采用了单项式-多项式的结构,使得代码更加灵活,易于扩展。采用了后缀表达式的计算方法,避免了TLE问题。
缺点:为了便于实现,将求导的计算操作都在Derivative类中实现,而没有对每个类型的因子设置一个求导方法,这样可能会使得代码的可扩展性降低。




可以看到Item类中的multiply方法复杂度较高,这是因为在Item类中实现了多项式的乘法操作,需要遍历两个多项式的所有单项式进行相乘并化简,复杂度较高。另外,SelfFunc类中的subFun方法复杂度也较高,这是因为subFun方法实现了对自定义函数的递归替换,需要进行的逻辑判断较多,该部分可以进一步优化。

如图所示,Item类、SelfFunc类和Simplify类的复杂度较高。其中Item类的复杂度较高是因为Item类实现了多项式的加减乘除等运算操作,代码量较大。SelfFunc类的复杂度较高是因为SelfFunc类实现了对自定义函数的递归替换,需要进行的逻辑判断较多。Simplify类则用于对最后的输出进行化简,需要的逻辑判断较多,复杂度较高。
hw3的优化吃了hw2的教训后就谨慎了很多,甚至对有的性能提升不明显的优化是能不做就不做,以防出bug...这次的性能分就得的比较低,不过也没出现bug,总体来说还能接受,之后也在讨论区见识了一下优化仙人的操作,果然自己还是有很大的提升空间的。
进行了代码重构之后,强测和互测均为出现bug。
hw1中要求的功能较为简单,而且在A房,估计bug出现率较低,并没有特别去进行hack。emmm不过事后发现还是有同组同学出现bug并被hack。
hw2时主要采用白箱测试以及特殊样例构造的hack策略。并结合代码和本地测试hack成功,发现同组同学的代码没有对类似(1-1)这样的样例进行特殊考虑的的情况,此时其输出为空,而没有考虑特殊输出0。
hw3时代码结构变得复杂,笔者选择了采用自动评测机进行黑箱测试,用评测机构造大量样例进行hack,并结合代码成功发现同组同学没有考虑自定义函数参数不按xyz顺序的情况的bug,构造样例hack成功。不过由于生成数据的随机性,发现很多时候还是自己手动构造特殊样例比起靠评测机撞大运效果会更好。
总结一下,评测机比较适合进行初步的基于大规模样例的hack,但其随机性还是具有局限性,进一步的hack还是需要手动构造特殊样例进行hack。(更是发现有同学能够卡着cost去构造让程序TLE的样例并且hack成功,笔者惊呼tql,看来hack也是门艺术)
第一单元的作业于笔者来说并不算顺利,甚至可谓是有点心惊胆战,也是让笔者快速过渡到紧张的OO学习状态,那一天人们又想起了被OO支配的恐惧(bushi)。
本单元的作业训练中让我深刻认识到构建一个良好的架构的重要性。第一次作业自认为选择了还不错的架构因为开始选择了正确的架构,在第二次作业的迭代没有出现太大范围的重构。但是!在第一次作业的研讨课上,当时听到分享的同学说计算过程中不合并同类项会TLE但是我并没有引起足够的重视,而是仍然选择之前简便的添加因子遍历乘到Term中的方法,导致第二次作业出现TLE问题。这就使得我在bug修复以及hw3中尝到了重构的苦头,改写了包括单项式-多项式、后缀表达式、栈计算等在内的大部分代码。
以后进行工程开发时,首先应该对架构有一个明确的认识和规划,并且考虑可能出现的负面影响(包括TLE),然后再进行操作,这样可以最大程度地避免大范围重构的可能,并且提高代码的可维护性和可扩展性。
希望在后续的OO旅程中,能够吸取第一单元的经验和教训,了解并掌握更多面向对象编程思想,实现更复杂代码的构建。
结合笔者本人经验,由于有选修OOpre课程的基础,在hw1的作业中上手还是比较顺畅的。能看得出来课程组也是对同学们寄予厚望,在hw2引入了较为复杂的功能(不过据我所知相较往年还是简单了不少),这也是对同学们的一次挑战。大家在hw2中或多或少都遇到了困难,感觉这部分的跃升还是难度偏大了些,让同学们有些措手不及。不过也需要承认这也是一次很好的锻炼,让大家在面对复杂问题时能够有更好的思考和解决问题的能力。依笔者个人拙见,还是可以设法增加一下理论课讲授内容和作业内容的关联性,尤其在理论课中希望老师不吝多讲解一些和作业类似的代码架构以及设计思路,这样可以让同学们更好地理解作业的要求,也能够更好地完成作业,以免在面对庞大的作业要求文档时不知所措。