301
社区成员
发帖
与我相关
我的任务
分享在大二下学期的第一个月,OO课程进行了第一单元的学习,主题为表达式的解析与化简。最终迭代完毕的作业的UML图如下:

其中,MainClass作为程序的入口,负责读入与最后结果的输出,Lexer类负责将表达式的内容标记为tokens,Parser类负责对表达式进行解析,SelfDefFunc类负责处理表达式中的自定义函数,Basic类负责存储每一个基本项,Poly类负责多项式的运算、化简以及转化为字符串,Factor接口下的各个类为tokens的不同类型。
下面我将以与迭代相同的过程对我的作业进行分析与总结。
题目要求为:读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为1层)的单变量表达式,输出恒等变形展开所有括号后的表达式。
受益于第一单元训练,采用了递归下降的方法进行解析与后续的化简。先将表达式解析成一个个token,再根据表达式中的符号将他们解析成Expr、Tern、Num、Var等类别,再通过Factor接口实现嵌套的作用。额外地,代码也可满足多层括号嵌套的功能。
在Poly类中,实现了多项式的运算以及化简(化简在运算过程中进行),储存多项式的数据结构选用了Hashmap(这也是为之后的重写埋下了伏笔),比较幸运的事我从此次作业开始对于指数部分就以BigInteger的方式进行存储,避免了之后指数超出int范围的麻烦。最后考虑到性能分数,加了一个通过调整多项式顺序来保证第一项非负的小功能。
第一次作业的难点我认为在于对递归下降法的熟悉与运用。对于我个人而言是第一次接触递归下降法,将每部分代码的作用以及代码与代码之间的联系完全理解是此次作业最大的挑战。在理解上述内容后,对于之后的迭代帮助非常大。
在第一次作业的完成过程中,由于代码量不大,所以顺便完成了评测机的搭建,直接调用python的sympy库进行输出的正确性检验。但是由于同room代码正确性以及性能上都很好,所以并未成功hack出bug。
在第一次作业的基础上,加入了指数函数exp(),以及自定义函数f(x, y, z)等。
借助于指数函数的出现,基本项在概念中的地位越来越重要,第一次作业的基本项为:系数*x^指数,而本次作业的基本项为:**{系数x^指数exp()}。将每一个小单位都看作基本项,再进行运算会带来很大的统一性与便捷性。由于第一次作业Poly类的数据结构埋下的坑,第二次作业改成了Hashmap<BigInteger, Hashmap<String, BigInteger>>的形式,最外层的键为指数,内层的Hashmap为<e的指数, 系数>。这对于表达式的运算和化简带来了不小的麻烦,一度有超时的错误。经过这次作业后我也下定决心要再次重写Poly类。
对于自定义函数,我使用的是在一开始通过字符串替换**的方式进行代入,对代入后的表达式再进行解析、运算、化简等处理。这样做的好处是代码的总体思路可以继承第一次作业,流程上也较为清晰。值得注意的是在实参代入形参的过程中很容易出现替换过多的情况,特别是对于按照先后顺序一个一个替换参数的做法,很容易把不该替换的换掉。我的解决方案是把自定义函数的变量提前替换成a, b, c,这样就没有此方面的顾虑了。
public static void main(String[] args) {
...
for (int i = 0; i < Integer.parseInt(numFunc); i++) {
String function = scanner.nextLine().replaceAll(" ", "")
.replaceAll("\t","").replaceAll("exp", "e").replaceAll("dx", "d");
String finalFunction = function.replaceAll("x", "a")
.replaceAll("y","b").replaceAll("z","c");
functions.addFunc(finalFunction);
}
...
}
我认为此次作业最大的难点为表达式的化简部分,我在这部分做得并不理想,这也是Poly类数据结构使用不当导致的,这次作业成功吸取了这方面教训,之后一定会慎重考虑再下手。
在第一次作业的评测机的基础上,加入了指数函数和自定义函数。同时不再使用sympy库进行检验,而是采用对拍的方式,这样可以同时对room里的所有代码进行检验。需要注意的是复杂度的问题,很容易弄超,导致好多样例提交失败。最终成功hack到两位同学的bug。
最后一次作业加入了求导算子,实现了表达式的求导功能。
此次作业对于上一次作业的迭代增量不大,但是由于本人第二次作业Poly类的数据结构使用不当,便进行了重写,改为了Arraylist,其中Basic为新添的基本项类,专门用于存储一个个基本项,最后在Poly类的运算中添加了求导的计算方法,然后本次作业的迭代就大体完成了。关于表达式的化简,在合并同类项时只需要比较两个Basic的指数与指数函数是否相同,即可判断是否合并,相较于第二次作业有了很大的改善。
在多个对于对代码性能需求较高的样例的测试中,发现在实现的Poly类乘法中零项出现次数很多,这导致在遍历Basic项时出现了巨大的时间浪费,于是在计算时添加了消除功能来清除零项:
public Poly mul(Poly otherPoly) {
ArrayList<Basic> result = new ArrayList<>();
for (Basic basic1 : this.expression) {
for (Basic basic2 : otherPoly.expression) {
BigInteger coeff = basic1.getCoeff().multiply(basic2.getCoeff());
if (coeff.equals(BigInteger.ZERO)) {
continue;
}
...
}
}
return new Poly(result);
}
经过优化后的代码性能得到了极大的改善。
经过第一单元的学习,我较为熟练地掌握了递归下降法的应用,并进一步完善了面向对象编程的能力。同时,我也深刻体会到了对代码进行正确性测试的重要性,这是保证代码质量最关键的一步。
| Basic.getIndex() | 0.0 | 1.0 | 1.0 | 1.0 |
|---|---|---|---|---|
| Basic.setCoeff(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
| Const.Const(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| Const.toPoly() | 0.0 | 1.0 | 1.0 | 1.0 |
| Const.toString() | 0.0 | 1.0 | 1.0 | 1.0 |
| Deractive.Deractive(Expr) | 0.0 | 1.0 | 1.0 | 1.0 |
| Deractive.toPoly() | 0.0 | 1.0 | 1.0 | 1.0 |
| Deractive.toString() | 0.0 | 1.0 | 1.0 | 1.0 |
| Exp.Exp(Expr) | 0.0 | 1.0 | 1.0 | 1.0 |
| Exp.toPoly() | 0.0 | 1.0 | 1.0 | 1.0 |
| Exp.toString() | 0.0 | 1.0 | 1.0 | 1.0 |
| Expo.addFactor(Factor) | 0.0 | 1.0 | 1.0 | 1.0 |
| Expo.Expo() | 0.0 | 1.0 | 1.0 | 1.0 |
| Expo.toPoly() | 2.0 | 2.0 | 2.0 | 2.0 |
| Expo.toString() | 3.0 | 1.0 | 3.0 | 3.0 |
| Expr.addTerm(Term) | 0.0 | 1.0 | 1.0 | 1.0 |
| Expr.Expr() | 0.0 | 1.0 | 1.0 | 1.0 |
| Expr.getOperators() | 0.0 | 1.0 | 1.0 | 1.0 |
| Expr.toPoly() | 9.0 | 1.0 | 6.0 | 6.0 |
| Expr.toString() | 3.0 | 1.0 | 3.0 | 3.0 |
| Lexer.getConst() | 2.0 | 1.0 | 3.0 | 3.0 |
| Lexer.getDerv() | 7.0 | 1.0 | 4.0 | 5.0 |
| Lexer.getExp() | 7.0 | 1.0 | 4.0 | 5.0 |
| Lexer.Lexer(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| Lexer.next() | 17.0 | 3.0 | 10.0 | 13.0 |
| Lexer.peek() | 0.0 | 1.0 | 1.0 | 1.0 |
| Lexer.standardize(String) | 22.0 | 1.0 | 9.0 | 9.0 |
| MainClass.main(String[]) | 1.0 | 1.0 | 2.0 | 2.0 |
| MyComparator.compare(Basic, Basic) | 0.0 | 1.0 | 1.0 | 1.0 |
| Parser.parseExpo() | 1.0 | 1.0 | 2.0 | 2.0 |
| Parser.parseExpr() | 1.0 | 1.0 | 2.0 | 2.0 |
| Parser.parseFactor() | 7.0 | 5.0 | 5.0 | 5.0 |
| Parser.Parser(Lexer) | 0.0 | 1.0 | 1.0 | 1.0 |
| Parser.parseTerm() | 1.0 | 1.0 | 2.0 | 2.0 |
| Poly.add(Poly) | 30.0 | 9.0 | 19.0 | 19.0 |
| Poly.derive() | 32.0 | 1.0 | 9.0 | 9.0 |
| Poly.getExpression() | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.mul(Poly) | 15.0 | 4.0 | 9.0 | 10.0 |
| Poly.Poly(ArrayList) | 0.0 | 1.0 | 1.0 | 1.0 |
| Poly.pow(Poly) | 1.0 | 1.0 | 2.0 | 2.0 |
| Poly.sub(Poly) | 30.0 | 9.0 | 19.0 | 19.0 |
| Poly.toString() | 59.0 | 7.0 | 18.0 | 22.0 |
| SelfDefFunc.addFunc(String) | 8.0 | 3.0 | 3.0 | 5.0 |
| SelfDefFunc.SelfDefFunc() | 0.0 | 1.0 | 1.0 | 1.0 |
| SelfDefFunc.simplifySelf(String) | 42.0 | 7.0 | 14.0 | 17.0 |
| Term.addExpo(Factor) | 0.0 | 1.0 | 1.0 | 1.0 |
| Term.Term() | 0.0 | 1.0 | 1.0 | 1.0 |
| Term.toPoly() | 1.0 | 1.0 | 2.0 | 2.0 |
| Term.toString() | 3.0 | 1.0 | 3.0 | 3.0 |
| Var.toPoly() | 0.0 | 1.0 | 1.0 | 1.0 |
| Var.toString() | 0.0 | 1.0 | 1.0 | 1.0 |
| Var.Var(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| Total | 304.0 | 95.0 | 186.0 | 201.0 |
| Average | 5.527 | 1.727 | 3.381 | 3.654 |
单一职责原则(SRP):在本次作业中,对表达式做了深度的解析,每一个类都有与众不同的职责,比如Lexer类负责解析运算基本元,Poly类负责项之间的基本运算等等。开闭原则(OCP):项目中对表达式中各个部分进行了精细的划分,无论增加何种运算和符号,都较为容易扩展。对于表达式的解析、运算、化简部分进行封装,不需要大量修改已经存在的代码。接口隔离原则(ISP):本项目中实现了Facotr接口,接口实现了如toString,toPoly等方法。这些方法并不是放在同一个类来实现的,而是按照每个类的需求来重写。依赖反转原则(DIP):Expr,Term相对于Factor来说都是高层次模块,但它们之间并没有过度的依赖,总体来说,Expr和Term并不依赖于因子之间相互运算和合并的细节,而是侧重于抽象的“表达式”和“项”方面。最后感谢课程组在作业方面的支持与帮助以及对于课程的精心设计。