301
社区成员
发帖
与我相关
我的任务
分享
第一单元的任务内容是:通过对数学意义上的表达式结构进行建模,完成多项式的括号展开与函数调用、化简,进一步体会层次化设计的思想的应用和工程实现。历经三次迭代,最终完成了这个单元的内容。最终的程序架构如下图所示。
其中Main为程序的入口,FuncDefiner解决了自定义函数的替换,Lexer和Parser分别完成了词法和语法的解析,实现了递归下降建立表达式树,中间的Exper、Term、Factor(Number、Expo、Der、Var)构成了表达式树,最终将表达式转化为单项式Item之和多项式Poly进行计算,再使用toStr输出计算结果。

对第三次迭代完成后的代码进行复杂度分析:
| 类名 | 属性个数 | 方法个数 | 代码规模 | OCavg | |
|---|---|---|---|---|---|
| 1 | Main | 0 | 1 | 17 | 2.00 |
| 2 | FuncDefiner | 0 | 2 | 54 | 5.00 |
| 3 | Lexer | 3 | 8 | 121 | 3.75 |
| 4 | Parser | 0 | 4 | 79 | 4.50 |
| 5 | Poly | 3 | 14 | 240 | 4.71 |
| 6 | Item | 3 | 9 | 88 | 2.56 |
| 7 | Expr | 1 | 5 | 48 | 1.80 |
| 8 | Term | 0 | 4 | 30 | 1.50 |
| 9 | Factor | 1 | 3 | 6 | - |
| 10 | Expo | 1 | 4 | 44 | 2.00 |
| 11 | Number | 1 | 4 | 19 | 1.00 |
| 12 | Der | 1 | 4 | 18 | 1.00 |
| 13 | Var | 1 | 4 | 25 | 1.00 |
通过数据发现,Lexer和Poly代码量较大,复杂度高。当第一次作业结束后,这一弊端并不明显,由于一次次的扩展,因子的类别逐渐复杂,处理的方法越来越多样,计算的方法逐渐复杂却仍集中在这两个类中,没有在适当的时机进行拆分,导致代码过于臃肿。
| method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| FuncDefiner.callFunc(String) | 12 | 1 | 7 | 8 |
| Item.toStr() | 20 | 1 | 13 | 13 |
| Lexer.getFactor() | 27 | 2 | 14 | 15 |
| Lexer.getIndex() | 12 | 1 | 5 | 6 |
| Parser.parseFactor() | 13 | 7 | 9 | 9 |
| Parse.parseTerm() | 7 | 1 | 5 | 5 |
| Poly.addItem(Item) | 13 | 4 | 7 | 7 |
| Poly.equPoly(Poly) | 21 | 11 | 9 | 14 |
| Poly.getResult() | 7 | 2 | 3 | 4 |
| Poly.isFactor() | 18 | 8 | 11 | 12 |
| Poly.toStr() | 21 | 7 | 11 | 11 |
表格中展示了复杂度较高的一些方法,其他方法的复杂度均较低。具体分析这些类中的每一个方法,仍可以看到其中有几个方法过于臃肿,都是使用次数最多,几次迭代中改动最大的方法。由于第一次作业完成时充分考虑了后续可扩展性的问题,最终代码并没有进行较大的重构,但由于代码不断扩展,也产生了诸多问题。例如最为臃肿的getFactor,由于Factor种类一次次扩展导致这一方法越来越复杂,不利于代码阅读和维护。如果要解决这一问题,可以将get不同Factor的方法进行拆分,建立新的类,将获取不同的Factor的分支拆分为不同的方法,降低代码的耦合度,使得结构更加清晰。此类问题可以在下一次作业中有所改进。(不得不说CodeMetrics插件很妙)

下面我将从三次作业迭代的角度讲述我的架构体验。
本次作业中需要完成的任务为:读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号后的表达式。
在本次作业中,展开所有括号的定义是:对原输入表达式$E$做恒等变形,得到新表达式$E'$且$E'$中不含有字符 ‘ ( ’ 和 ‘ ) ’ 。
这次作业的内容经过实验的启发决定使用递归下降法,经过上学期的启发,一个不可扩展的程序就是一切厄运的来源,因此架构过程十分谨慎。在开始时,阅读了很多往届博客,吸取了很多经验。其中Lexer类和Parser类与实验中提供的代码的功能能相似:Lexer负责解析表达式中的因子、符号等,将解析出来的内容传递到Parser中。Parser中使用递归下降解析构建树,将解析的表达式转化为多项式。
多项式由若干个单项式组成,单项式的基本结构是:$$ {co}*x^{index} $$故多项式为:$$ {co}_1*x^{index}_1+{co}_2*x^{index}_2+……$$最终再将多项式转化为字符串进行输出。

本次作业内容比较简单,优化可以做的工作有将正项放在字符串的第一项可以减少符号数、0项不输出、0指数项为1等。
由于优化第一项前符号,在toString输出字符串时对内容进行了修改,后续造成了骇人的灵异事件。需时刻注意不同方法对内容操作的权限问题。在互测过程中,大家的代码基本没有问题,需要注意的可能是含0的一些特殊的点,如0、x^0、0*0等等。
本次作业中需要完成的任务为:读入一系列自定义函数的定义以及一个包含幂函数、指数函数、自定义函数调用的表达式,输出恒等变形展开所有括号后的表达式。
在本次作业中,展开所有括号的定义是:对原输入表达式$E$做恒等变形,得到新表达式$E'$。其中,$E'$中不再含有自定义函数,且只包含必要的括号。
这次作业新增的内容很多,但由于第一次架构相对合理,减少了很多工作量,比如括号的嵌套在上一次作业中就可以进行处理。一系列自定义函数可以通过FuncDefiner进行解析和存储,在遇到f、g、h时进行参数的替换,注意到参数中可能会出现多层括号以及嵌套调用,我采用括号匹配加逗号分隔的方法来进行变量替换,遇到自定义函数时直接将input替换为表达式并将pos调到起始位置,即使有嵌套调用也可以顺序处理。指数函数则将其括号内的内容以表达式的形式存储并同理用index存储指数,在转换为表达式时再展开计算。
由于此次作业添加了指数因子,故添加了Expo类,但由于指数函数的指数也是一个Expr,故和Expr十分相似。但单项式变得更加复杂:
$${co}*x^{index}*e^{exp}$$ 这一问题导致后续加法计算时不仅要比较index是否相同,更要比较exp是否相同,而exp是Poly类型,比较Poly也是在比较Item每一项是否相同,此处需要递归处理。而乘法计算相对比较简单,系数相乘,指数相加即可。

此次作业由于指数函数的增加导致表达式复杂度急剧上升,优化方式变幻莫测,我个人想到的是提公因式,看到讨论区的帖子又进行了进一步的优化。在优化过程中遇到了一些小问题比如提取公因式后括号内变为因子可以减少一层括号没考虑到以及提取1时也将其算在指数的长度内导致最终优化的效果并没有实现这种方法的最优效果。
另外的优化就是exp内因子只保留单层括号的问题,在判断是否为因子时,还需要仔细考量。
此次优化使Poly类的代码量和复杂度显著提升,分支数量增多导致问题也更加多样,在优化的过程中也要尽量保持代码的流畅性和可读性。
此次我在计算有指数的exp时出现了深/浅克隆的问题,在乘的过程中改变了原有的乘数导致计算错误。函数的exp替换时把“exp”的‘x’也当成参数进行了替换,修改方法是先将x替换为无关字母再在变量替换后换回,虽然比较笨拙,但也可以避免这种问题。但由于递归式的比较,程序耗费时间可能较长。
互测环节出现的问题主要集中在优化上,例如非因子去括号、提取负指数等等,可见在优化面前还是正确性更加重要一些。
本次作业中需要完成的任务为:读入一系列自定义函数的定义以及一个包含幂函数、指数函数、自定义函数调用、求导算子的表达式,输出恒等变形展开所有括号后的表达式。
在本次作业中,展开所有括号的定义是:对原输入表达式$E$做恒等变形,得到新表达式$E'$。其中,$E'$中不再含有自定义函数,不再含有求导算子,且只包含必要的括号
此次作业只加了自定义函数中包含已定义的自定义函数,如$$ f(x)=x,g(y)=f(y)$$
这个功能在上一次作业中基本可以实现,故不需要再添加内容;对于导数因子则需要额外添加求导的算法,这里我的实现方法是将其中表达式转换为多项式,再对多项式的每一个单项式逐个进行求导,单项式的形式和上一次一致,对于单项式的求导只需要使用链式法则,这一点在Item.diffItem()实现,此时需要额外注意项中是否包含co、index、exp的特殊情况。

此次能做的优化和上次大致相同,可以保存不动。我的代码优化存在一些问题,可惜最有一次提交也没有发现解决,有些遗憾。
求导时需要对特殊情况加以处理,如dx(0)等等。此次作业中被hack出现了TLE问题,发现在遇到多层exp嵌套时运行时间过长,在计算深层嵌套表达式时,我的算法还有很大的提升空间。
此外互测存在的bug也有出现在栈空间溢出、优化问题等。
假设第四次迭代情景,允许指数为负数,添加对数函数
指数函数 → 'ln' 空白项 '(' 空白项 因子 空白项 ')' [空白项 指数]
指数 → '^' 空白项 ['+'] 允许前导零的整数 (可以为负数)
此时单项式的形式变得更加复杂 $${co}*x^{index}*e^{exp}*ln(log)$$但架构还可以实现,由于指数可以为负数,优化变得更加多样,策略肯定也会不同一些。但目前的代码架构还可以满足情景需求。
经历了三次迭代,充分体会到了良好架构对于后续作业的影响,可见充分的思考的是能减少重构的前提。而有些问题明明是易错问题,却总是重犯,例如深浅克隆的问题,即使已经注意,但最后还是在计算指数时遇到相同的问题,可见坑只有掉进去才深有体会。在测试过程中充分考虑特殊样例、边界压力测试样例等等才有可能让代码更加可靠。
对课程没有太多建议,整个过程下来感觉第一、二次作业的压力最大,第二次作业量则与第一次架构息息相关。第一次的作业中递归下降法的文章指导和实验代码的架构都是顺利完成必要的帮助,尽早的阅读学习可能会让第一次作业少一些局促吧。
对自己的话希望能在类和方法的创建过程中更加慎重,仔细考虑方法见的调用关系,在完成的过程中尽量维持代码的可读性,保持好的风格,让代码更加简洁清晰。