第一单元作业总结
第一单元作业总结
BUAA OO 2021面向对象第一次作业
程序架构
第一次作业
作业要求概述
表达式求导,表达式中只包含由常数因子和变量因子(形如$x^{n}$,n为常数),符号与符号、符号与因子之间可能有空格。没有括号。
架构思路
模式识别
根据题目中的定义的方式,分别写出符号、空白字符、常数的正则表达式,再通过这些基本正则表达式,进一步构建常数因子、变量因子、项的正则表达式。
此处需注意三点:
- 不要采取一个表达式中包含很多字符的长式形式,而要借用基础表达式来定义进阶表达式
- 注意正则表达式的语法规则,很多地方需要加括号。
- 由于正则表达式识别过程中,递归层数较多可能引起爆栈问题,要注意使用非捕获组。
存储方式
以项为单位进行存储,每一项由系数、指数两个参数构成。注意使用大整数(BigInteger)类型。
求导
对于每一项,修改指数与系数既可以。
项的合并
直接利用hashmap即可。(merge函数非常好用)
分析
主观分析
由于正则表达式的限制,此方法在处理较长项时会爆栈(比如500个x相乘)。
度量分析
| Method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Expression.Expression(String) | 7 | 1 | 4 | 4 |
| Expression.toString() | 11 | 4 | 3 | 6 |
| MainClass.main(String[]) | 0 | 1 | 1 | 1 |
| Term.Term(String,String) | 2 | 1 | 1 | 2 |
| Term.calculate() | 2 | 1 | 2 | 2 |
| Term.getCoeAndIndex(String) | 2 | 1 | 2 | 2 |
| Term.getCoefficient() | 0 | 1 | 1 | 1 |
| Term.getFinalIndex() | 0 | 1 | 1 | 1 |
| Term.getTerm() | 0 | 1 | 1 | 1 |
| Term.getTermFinalIndex(String) | 1 | 2 | 2 | 2 |
| Term.multiple() | 16 | 1 | 6 | 6 |
| Term.setCoefficient(BigInteger) | 0 | 1 | 1 | 1 |
| Term.setFinalIndex(BigInteger) | 0 | 1 | 1 | 1 |
| Term.setTerm(String) | 0 | 1 | 1 | 1 |
| Term.toString() | 9 | 8 | 9 | 9 |
| Test.checkAddAndSub(String) | 1 | 1 | 2 | 2 |
| Test.checkConstantFactor(String) | 1 | 1 | 2 | 2 |
| Test.checkEmpty(String) | 1 | 1 | 2 | 2 |
| Test.checkIndex(String) | 1 | 1 | 2 | 2 |
| Test.checkInteger(String) | 1 | 1 | 2 | 2 |
| Test.checkPower(String) | 1 | 1 | 2 | 2 |
| Test.checkSignInteger(String) | 1 | 1 | 2 | 2 |
| Test.main(String[]) | 7 | 1 | 5 | 5 |
| null.compare(BigInteger,BigInteger) | 0 | 1 | 1 | 1 |
| Class | OCavg | OCmax | WMC | |
| Expression | 3.67 | 6 | 11 | |
| MainClass | 1 | 1 | 1 | |
| Term | 2.33 | 8 | 28 | |
| Test | 2.38 | 5 | 19 | |
| Package | v(G)avg | v(G)tot | ||
| 2.5 | 60 | |||
| Module | v(G)avg | v(G)tot | ||
| HW1 | 2.5 | 60 | ||
| Project | v(G)avg | v(G)tot | ||
| project | 2.5 | 60 |
multiple方法和toString方法分别是负责表达式拆分处理和输出的方法,是本程序复杂度的主要承担者。

第二次作业
作业要求概述
在上述基础上增加括号嵌套,和三角函数,但三角函数中不可嵌套。
架构思路
模式识别
与上述识别类似,但本次不再识别项,而是拆分到因子级别即停止。
此处需注意两点:
- 注意连续的符号处理,比如括号和加减称号的相连
- 注意符号合法性的判定
存储方式
以项为单位存储,每一项以形如$ax{b}sinxcos^dx$的形式存储,需要存储a、b、c、d四个整数。
项的提取
目标:将一个表达式处理后得到一个由项构成的列表。
-
括号匹配,匹配方式是从左向右遍历,遇到左括号;入栈,遇到右括号,弹出栈顶的左括号,与之匹配。
-
将括号中的内容看成一个新的表达式,递归处理
-
遇到加号则将后续处理得到的列表加入列表中,遇到减号则将系数取反后加入列表中,遇到乘号则将前一项所产生的列表与新得到的相乘。
两种具体实现如下:
- 此处可以用列表下再存列表的形式。
- 但其实不需要那么麻烦,只需要预存该符号前的两项即可。
-
全部处理结束后,将列表化简,回溯。
需要注意的是,由于括号的出现,每一项实际上不是单纯的项,而可能是一个列表。
求导
对于每一项,修改指数与系数就可以了。
项的合并
直接利用hashmap即可。(merge函数非常好用)
分析
主观分析
个人觉得这是处理本次作业较好的方法,方便合并同类项。但是缺乏三角函数的合并。
度量分析
| Method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Expression.Expression(String) | 0 | 1 | 1 | 1 |
| Expression.convertFromTerm(Term) | 0 | 1 | 1 | 1 |
| Expression.getPre() | 0 | 1 | 1 | 1 |
| Expression.solve() | 1 | 1 | 2 | 2 |
| Expression.toString() | 11 | 4 | 3 | 6 |
| MainClass.main(String[]) | 0 | 1 | 1 | 1 |
| MultiTerm.MultiTerm(String) | 0 | 1 | 1 | 1 |
| MultiTerm.arrMultiply(ArrayList |
3 | 1 | 3 | 3 |
| MultiTerm.arrRe(ArrayList |
1 | 1 | 2 | 2 |
| MultiTerm.cal() | 1 | 1 | 2 | 2 |
| MultiTerm.getNewTerms() | 0 | 1 | 1 | 1 |
| MultiTerm.getPos() | 4 | 1 | 4 | 4 |
| MultiTerm.getTerms() | 0 | 1 | 1 | 1 |
| MultiTerm.mergeThird() | 14 | 3 | 7 | 7 |
| MultiTerm.mergeTwo() | 5 | 3 | 3 | 4 |
| MultiTerm.preFunInv(char) | 2 | 3 | 1 | 3 |
| MultiTerm.solve() | 17 | 1 | 7 | 7 |
| Result.equals(Object) | 4 | 3 | 4 | 6 |
| Result.getCosIndex() | 0 | 1 | 1 | 1 |
| Result.getSinIndex() | 0 | 1 | 1 | 1 |
| Result.getXsIndex() | 0 | 1 | 1 | 1 |
| Result.hashCode() | 0 | 1 | 1 | 1 |
| Result.setCosIndex(BigInteger) | 0 | 1 | 1 | 1 |
| Result.setSinIndex(BigInteger) | 0 | 1 | 1 | 1 |
| Result.setXsIndex(BigInteger) | 0 | 1 | 1 | 1 |
| Result.toString(BigInteger) | 22 | 2 | 10 | 17 |
| Term.Term(String) | 0 | 1 | 1 | 1 |
| Term.calculate() | 3 | 1 | 4 | 4 |
| Term.getCoe(String) | 0 | 1 | 1 | 1 |
| Term.getCoeAndIndex() | 11 | 1 | 8 | 8 |
| Term.getCoefficient() | 0 | 1 | 1 | 1 |
| Term.getCosIndex() | 0 | 1 | 1 | 1 |
| Term.getInd(String) | 1 | 2 | 2 | 2 |
| Term.getSinIndex() | 0 | 1 | 1 | 1 |
| Term.getxIndex() | 0 | 1 | 1 | 1 |
| Term.setCoefficient(BigInteger) | 0 | 1 | 1 | 1 |
| Term.setCosIndex(BigInteger) | 0 | 1 | 1 | 1 |
| Term.setSinIndex(BigInteger) | 0 | 1 | 1 | 1 |
| Term.setxIndex(BigInteger) | 0 | 1 | 1 | 1 |
| Term.singleMultiply(Term) | 0 | 1 | 1 | 1 |
| Test.checkAddAndSub(String) | 1 | 1 | 2 | 2 |
| Test.checkConstantFactor(String) | 1 | 1 | 2 | 2 |
| Test.checkIndex(String) | 1 | 1 | 2 | 2 |
| Test.checkInteger(String) | 1 | 1 | 2 | 2 |
| Test.checkPower(String) | 1 | 1 | 2 | 2 |
| Test.checkSignInteger(String) | 1 | 1 | 2 | 2 |
| Test.checkSin(String) | 1 | 1 | 2 | 2 |
| Test.main(String[]) | 7 | 1 | 5 | 5 |
| Class | OCavg | OCmax | WMC | |
| Expression | 2.2 | 6 | 11 | |
| MainClass | 1 | 1 | 1 | |
| MultiTerm | 3.09 | 7 | 34 | |
| Result | 2.78 | 15 | 25 | |
| Term | 1.79 | 8 | 25 | |
| Test | 2.38 | 5 | 19 | |
| Package | v(G)avg | v(G)tot | ||
| 2.52 | 121 | |||
| Module | v(G)avg | v(G)tot | ||
| HW1 | 2.52 | 121 | ||
| Project | v(G)avg | v(G)tot | ||
| project | 2.52 | 121 |
可以看出,toString方法的压力较大。

第三次作业
作业要求概述
在上述基础上增加三角函数内括号嵌套,三角函数的乘方,合法性检查。
架构思路
本次思路与前两次有较大不同。
模式识别
与上述识别类似,但本次不再识别项,而是拆分到因子级别即停止。
存储方式
以表达式树的形式进行存储。特别的,sin、cos、括号视为单目运算符。
设置TreePoint基类,作为树节点。
根据节点类型,设置双目运算符类、单目运算符类、常数类、变量因子类等。
项的提取
目标:将一个表达式处理后得到一个表达式树。
- 进行因子检查,找到第一个出现的因子并开始处理
- 将括号中的内容看成一个新的表达式,递归处理
- 按照符号特性建树
- 处理结束后,回溯并返回树的根节点。
求导
从根节点开始递归求导。可以按照链式求导法则很好地建出一棵新的导函数树。
分析
主观分析
个人觉得此方法处理繁琐,不易化简。合法性判断细节繁多。但是可以很方便地使用链式求导法则进行求导。
度量分析
| Method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Bracket.Bracket() | 0 | 1 | 1 | 1 |
| Bracket.calculate() | 1 | 1 | 2 | 2 |
| Bracket.getSon() | 0 | 1 | 1 | 1 |
| Bracket.setSon(TreePoint) | 0 | 1 | 1 | 1 |
| Bracket.toString() | 3 | 3 | 2 | 4 |
| Constant.Constant() | 0 | 1 | 1 | 1 |
| Constant.calculate() | 0 | 1 | 1 | 1 |
| Constant.getValue() | 0 | 1 | 1 | 1 |
| Constant.setValue(BigInteger) | 0 | 1 | 1 | 1 |
| Constant.toString() | 0 | 1 | 1 | 1 |
| Expression.getLine() | 0 | 1 | 1 | 1 |
| Expression.setLine(String) | 0 | 1 | 1 | 1 |
| Expression.solve() | 0 | 1 | 1 | 1 |
| Func.Func() | 0 | 1 | 1 | 1 |
| Func.calculate() | 2 | 2 | 2 | 2 |
| Func.getFunc() | 3 | 4 | 3 | 4 |
| Func.getLeftSon() | 0 | 1 | 1 | 1 |
| Func.getRightSon() | 0 | 1 | 1 | 1 |
| Func.setLeftSon(TreePoint) | 0 | 1 | 1 | 1 |
| Func.setRightSon(TreePoint) | 0 | 1 | 1 | 1 |
| Func.toString() | 41 | 6 | 12 | 17 |
| MainClass.main(String[]) | 1 | 1 | 2 | 2 |
| MatchType.MatchType(int,int,int,int,int,TreePoint) | 0 | 1 | 1 | 1 |
| MatchType.getBracketEnd() | 0 | 1 | 1 | 1 |
| MatchType.getBracketStart() | 0 | 1 | 1 | 1 |
| MatchType.getEnd() | 0 | 1 | 1 | 1 |
| MatchType.getStart() | 0 | 1 | 1 | 1 |
| MatchType.getTreePoint() | 0 | 1 | 1 | 1 |
| MatchType.getType() | 0 | 1 | 1 | 1 |
| MatchType.setBracketEnd(int) | 0 | 1 | 1 | 1 |
| MatchType.setBracketStart(int) | 0 | 1 | 1 | 1 |
| MatchType.setEnd(int) | 0 | 1 | 1 | 1 |
| MatchType.setStart(int) | 0 | 1 | 1 | 1 |
| MatchType.setTreePoint(TreePoint) | 0 | 1 | 1 | 1 |
| MatchType.setType(int) | 0 | 1 | 1 | 1 |
| Power.Power() | 0 | 1 | 1 | 1 |
| Power.calculate() | 0 | 1 | 1 | 1 |
| Power.getIndex() | 0 | 1 | 1 | 1 |
| Power.setIndex(BigInteger) | 0 | 1 | 1 | 1 |
| Power.toString() | 2 | 3 | 2 | 3 |
| Tree.calculate() | 0 | 1 | 1 | 1 |
| Tree.getLine() | 0 | 1 | 1 | 1 |
| Tree.getNewRoot() | 0 | 1 | 1 | 1 |
| Tree.getRoot() | 0 | 1 | 1 | 1 |
| Tree.judgeAS(int,int) | 8 | 6 | 5 | 7 |
| Tree.matchBracket(int) | 13 | 6 | 5 | 8 |
| Tree.matchExpression() | 12 | 6 | 6 | 9 |
| Tree.matchFac(int) | 5 | 4 | 4 | 7 |
| Tree.matchPower(int) | 7 | 4 | 4 | 5 |
| Tree.matchSignInt(int) | 1 | 2 | 2 | 2 |
| Tree.matchTerm(int) | 8 | 6 | 3 | 6 |
| Tree.matchTri(int) | 33 | 9 | 10 | 14 |
| Tree.setLine(String) | 0 | 1 | 1 | 1 |
| Tree.setNewRoot(TreePoint) | 0 | 1 | 1 | 1 |
| Tree.setRoot(TreePoint) | 0 | 1 | 1 | 1 |
| Tree.solveFac(MatchType) | 19 | 10 | 9 | 12 |
| TreePoint.TreePoint() | 0 | 1 | 1 | 1 |
| TreePoint.calculate() | 5 | 6 | 6 | 6 |
| TreePoint.create(Integer,Integer) | 0 | 1 | 1 | 1 |
| TreePoint.getFactor() | 0 | 1 | 1 | 1 |
| TreePoint.getFunction() | 0 | 1 | 1 | 1 |
| TreePoint.setFactor(Integer) | 0 | 1 | 1 | 1 |
| TreePoint.setFunction(Integer) | 0 | 1 | 1 | 1 |
| TreePoint.toString() | 5 | 6 | 6 | 6 |
| TriFun.TriFun() | 0 | 1 | 1 | 1 |
| TriFun.calculate() | 2 | 1 | 3 | 3 |
| TriFun.getIndex() | 0 | 1 | 1 | 1 |
| TriFun.getSon() | 0 | 1 | 1 | 1 |
| TriFun.getTriType() | 0 | 1 | 1 | 1 |
| TriFun.setIndex(BigInteger) | 0 | 1 | 1 | 1 |
| TriFun.setSon(TreePoint) | 0 | 1 | 1 | 1 |
| TriFun.setTriType(Integer) | 0 | 1 | 1 | 1 |
| TriFun.toString() | 11 | 7 | 7 | 9 |
| Class | OCavg | OCmax | WMC | |
| Bracket | 1.8 | 4 | 9 | |
| Constant | 1 | 1 | 5 | |
| Expression | 1 | 1 | 3 | |
| FormatException | n/a | n/a | 0 | |
| Func | 3.5 | 17 | 28 | |
| MainClass | 1 | 1 | 1 | |
| MatchType | 1 | 1 | 13 | |
| Power | 1.4 | 3 | 7 | |
| Tree | 4.12 | 12 | 66 | |
| TreePoint | 2.25 | 6 | 18 | |
| TriFun | 2 | 8 | 18 | |
| Package | v(G)avg | v(G)tot | ||
| 2.48 | 181 | |||
| Module | v(G)avg | v(G)tot | ||
| HW3 | 2.48 | 181 | ||
| Project | v(G)avg | v(G)tot | ||
| project | 2.48 | 181 |
可以看出toSring、MatchTri过于复杂。由于toString中加入了过多的判断,导致效率过慢,而MatchTri中由于需要特殊处理三角函数中只能含有因子而不能含有项这一要求而十分繁琐。

bug分析
分析自己的bug
第一、二次作业在公测和互测中未发现bug。但在测试结束后,发现在500个x连乘过程中发现会爆栈。
第三次作业出现由于优化造成的负号丢失和括号较长时运行超时两大问题。
发现bug的策略
- 测试数据的上限和下限
- 测试连乘、连加、连减、加减混合情况
- 测试符号连续出现的情况
- 测试空格出现的不同位置
- 测试行末空格
- 混入其他字符
- 将sin、**拆开
重构经历
本单元作业共经历了六次大规模重构。
在第一次作业中,未出现重构情况。
在第二次作业中,由于细节考虑不周全出现了三次重构。
在第三次作业中,出现了两次重构。
值得一提的是,出了在上述程序架构中所述的三种方案,我还有另一种架构思路。我尝试不用递归,而使用栈来处理表达式,将其拆分后,建成表达式树的方法。由于栈的线性结构,理论上可以规避一些因为递归带来的风险和降低复杂度。大体思路如下:
- 初始栈为空
- 遇到常数因子或变量因子,则暂存
- 遇到+、-、*、**、sin(、cos(、(、)
- +、-:将前面所有+、-、*符号弹出,并根据暂存的因子做对应的建树操作
- $$:将前面所有符号弹出,并根据暂存的因子做对应的建树操作
- **:直接建树
- sin(、cos(、(:入栈
- ):弹栈直到sin(、cos(、(
- 全部处理结束后,将栈内剩余符号进行组装,成树
心得体会
首先,请让我从完成作业这一部分说起。
本单元做下来,感觉难度很大,一方面是由于解决此类问题本身的繁琐性,另一方面是由于对于面向对象的思想并不熟悉,直接上手很容易走歪,这是多次重构的一大原因。
在写第一、二次作业时完全没有预料到后续的发展,因而每次开启新的作业时都需要重构。在完成过程中,由于缺乏整体性设计思路,我个人想过也尝试过很多种方法,这也导致了大大小小多次重构。
我仔细分析了三次作业,我认为多次重构还有一个客观原因是:第三次作业的方法放在第一次作业中不仅不方便,而且难以优化,不是解决第一次作业的好方法。也就是说,三次作业的要求导向虽在形式上有统一之处,但其实在实际工程要求上是具有分歧的。
您可能会反驳说,这只是数据规模和形式上的差别,本质上是一样的。
但我不赞成您的观点,在工程上,数据规模和形式将直接影响具体方法的选择。失之毫厘则谬之千里,千级的数据和万级的数据是不可同日而语的,正是这一点细小的差别决定了算法的复杂度,两种代码可能将采取完全不同的思路。
其次,我想说说互测的问题。
由于学习进度紧张,我几乎没读过几份别人的代码,没有太多时间细读。互测是一个很好的环节,但是,由于周一周二课程很多,我在互测环节参与很少,这是一件很遗憾的事。
最后,我想说说对于课程安排的看法。我理解课程组设计作业的良苦用心,也并不否认我在其中有所收获。但是,我认为课程压力过大。
我不否认多次重构对水平的提高,但我认为在这样短的时间内多次重构实在让人抓狂。我认为,课程组所给出的作业周期过于紧凑是造成我个人压力增大、出现抑郁情绪的主要原因。在每周三开放作业,周四一天满课,还要准备第二天的实验,周五也只有晚上有空闲,周六、周日常常被各类竞赛或活动占据,所以,周日晚上上交作业的DDL其实很紧张。周一周二课程较多,还要完成其他作业,想要参与互测,是很难的。如果这一周强测或互测中出现问题,那么还需要再拿出一两天进行修改,这部分时间也是很难挤出来的。
我认为课程组应该考虑适当放宽作业时限,加长作业周期,删减作业次数。如果让我用三周直接去写一个第三次作业,那么我认为要比现在这种所谓的“过渡”方式要好一些。
我建议四周一个作业周期可以这样安排:两周写作业、反复构思、可以提交多个不同的思路版本、多角度测试和评价+一周互测,互相对比和分析+一周修复bug和总结反思。

浙公网安备 33010602011771号