301
社区成员
发帖
与我相关
我的任务
分享第一单元的核心内容有二:表达式解析 与 表达式化简。类图如下(已忽略不那么重要的方法、属性,标有 <<Parsed>> 的类由 Parser 生成对象)

我的设计的核心在于,将因子直接并全部视为多项式,同时 Expr 与 Term 也均展开为多项式。全部的计算(加、减、乘、合并、展开等)均在单项式与多项式类间完成。下图具体展示了多项式类是如何组织的。值得注意的是,单项式中出现的 e 指数,实际也为多项式,这里产生了递归的关系。

在 第二次作业 自定义函数 的处理中,我通过 Parser 解析出函数入口,并在调用时通过 函数入口 生成 函数调用,将实参作为 多项式 乘入,我认为这样的设计是非常好的,既避免了字符串替换的种种问题,也使其可以直接适用第三次作业中的嵌套调用。

在 第三次作业 求导因子 的处理中,我将其简化,直接在多项式与单项式两个类中增添方法进行计算,上图直观、简洁地展现了整个计算过程。
在这里我通过几个问题思考并分析了一下自己的设计。
几个因子为什么是继承而不是接口? 因为我考虑到它们要想参与运算,就必须要得到一个统一的对象(也就是我们计算得到的多项式),既然这样,与其利用接口规定一个 “展开成多项式” 的方法,倒不如直接让他们成为多项式的子类,在创建对象时,即成为多项式。这样设计更加清晰、统一,架构也更间接、易懂。
为什么不将求导因子视为因子? 经过研讨课的讨论,我才意识到还可以把求导当成因子()当时写的时候有点上头,没经过仔细地思考,便直接添加了两个方法完成第三次作业。现在想一想,将它们视为因子能够简化单项式与多项式类的功能,也使它们的功能更统一,有利于后续的拓展。
为什么不采用字符串替换的方式调用函数? 当时觉得使用字符串替换不够优雅,现在觉得使用字符串替换比较危险,容易出事。
深浅拷贝的问题如何解决? 我将多项式与单项式类设计成了 几乎不可变,“几乎” 是因为单项式类有一个方法会改变自身状态,但属特殊情况。此外,一切的加、乘、乘方、合并同类项,都会返回新的对象。
再增加一点内容还能应对嘛? 如果还是像求导这种可以直接计算的,比如求和符号,直接新增因子类,直接展开为多项式,无需过多处理。如果是像 e 指数这种新增内容的,比如 sin,需要对单项式新增属性,并且修改相应的合并检测。
| 文件 | 总行数 | 代码行 | 注释行 | 方法数 |
|---|---|---|---|---|
Monomial.java | 303 | 253 | 20 | 21 |
Polynomial.java | 175 | 154 | 0 | 17 |
Parser.java | 136 | 123 | 0 | 9 |
Expr.java | 49 | 37 | 1 | 4 |
| Total | 898 | 754 | 23 | -- |
可以看到,重点还是在于 计算与解析,单项式类与多项式类承载了较多的计算方法以及构造方法,因此代码较长、方法较多。
| Method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
expr.factor.Monomial.toExpString() | 28 | 13 | 13 | 15 |
expr.factor.Monomial.toOptimalExpString(Polynomial) | 20 | 7 | 8 | 11 |
expr.factor.Monomial.toString() | 15 | 2 | 10 | 12 |
expr.factor.Polynomial.equals(Polynomial) | 20 | 12 | 5 | 12 |
expr.factor.Polynomial.mergeLikeMonomial() | 8 | 4 | 5 | 5 |
expr.factor.Polynomial.toString() | 8 | 2 | 6 | 6 |
Lexer.Lexer(String) | 14 | 1 | 9 | 9 |
Parser.parseFactor() | 9 | 6 | 9 | 9 |
| Average | 2.13 | 1.59 | 2.17 | 2.39 |
可以看到,单项式类的 toExpString 方法复杂度异常高,是因为里面的分支太多,exp(a * x^b),里面的 a 和 b 都有很多情况,后续可以考虑再将它们进行拆分。
单项式类的 toOptimalExpString 方法也很复杂,因为它需要承载指数的优化(提公因数),其实没有什么很好的优化想法,仅仅是找一些公因数,尝试提取,比较长度。为了防止优化超时,通过静态属性限制计算次数。
另一个比较复杂的方法是多项式合并相关的(判断相等与合并同类项),因为前期设计使用 ArrayList 进行每一项的存储,在判断相等时需要使用嵌套循环,以致于复杂度较高的代码。这也导致了一些 Bug 的产生,所幸及时发现。
我认为我结构设计的耦合度还是相对较低的,因子们被 Parser 解析后便直接成为 Polynomial 的对象,全部的计算也包含在多项式与单项式中,耦合度较低。
例外是 e 指数,因为指数上也可能是多项式(即使不是,也当成多项式),这里的耦合较高,但似乎不可避免。
整个架构,以 Polynomial 为核心,从解析到各因子,最终归结为多项式类,整体架构较为清晰,耦合度较低。
在三次作业中,我的代码没有在强测与互测中出现错误。那就说一说在编程时遇到的 Bug 吧。
exp(0); exp(1); exp(2) 都是错的?还是不同的错误?Git 提交差异如下,很难评,当时还调试了半天,以为是算错了啥的。
i 写成了 j,this 写成了 that,在某些情况下会越界。调试也很简单,定位越界位置,一下子就发现笔误了。exp(..)^-1?? 因为我的自动测评姬没有文法检查,要不是多看了一眼,说不定就爆炸了。。。适度优化益脑,沉迷优化伤身。。。在互测中,我刀人的力度不大,比较难蚌的是 第二次作业 盲刀刀中两人(我真的只是想随便交一个样例看一看自己在哪一组的 ˚‧º·(˚ ˃̣̣̥᷄⌓˂̣̣̥᷅ )‧º·˚)
具体的 Bug 在于,exp 中为空(也就是 0)时输出异常,指数相同的 exp 被消去后,输出时越界。后通过自动测评姬轰炸时,没有发现异于此的 Bug,但事实上,有同学会出现 exp(-x) 这样的输出,我没有发现(因为这个在数学上是对的())。
第三次作业,核心在于借助 cost 的计算卡 TLE,很考验同学们代码的设计,据此刀中一人。
第一次作业整体迭代还是比较顺利的,整个过程也没有遇到很棘手的问题,我觉得可能得益于 前期架构设计 得较好,以及对于 不可变对象的设计 使得浅拷贝的问题不存在。后者是我这一次作业学到的一种设计思路。同时以及 层次化设计 的思路与架构,让我对这些设计理念也有了一个新的认识。
我觉得这一次作业对我的另一个帮助是让我对于 递归下降 有了更加深刻的理解,以及实践经验。我觉得以后还可以在这方面有更多的体会。
关于课程之后的建议,我觉得可以平衡一下几次作业之间的难度与复杂度,比如第二次与第三次作业之间的难度差距较大(当然这个可能也是课程组的考虑与设计)。