BUAA OO 第一次作业总结

BUAA OO 第一次作业总结

第一次作业

简介

第一次作业是简单的多项式求导。

UML 图与类结构

其中,各个类的含义如下:

|- MainClass:主类
|- expression (package):表达式包
    |- Factor (abstract class):因子类
        |- Constant:常数类
        |- Var:变量类
        |- Term:项类
        |- Expr:表达式类
|- parser (package):表达式解析包
    |- Parser:语法分析器
    |- Tokenizer:词法分析器
        |- TokenType (enum):词法单元类型
|- Pair:元组

最重要的抽象类 Factor 共三个方法:

  • abstract Factor derivate():求导
  • abstract String display():打印当前因子
  • abstract Factor simplify():化简因子

架构分析

我的代码分为了两个 package,其中 parser 用于解析输入,expression 用于处理表达式。除此之外,还有一个辅助的工具包 Pair。这个架构还是比较直观的,分类方法是“因子类型”或者说是“求导法则”。

设计理念

我首先用递归下降法解析输入,然后对得到的表达式进行第一次化简。之所以这里要插入一次求导,是因为求导的时候表达式可能会急剧膨胀,先化简有利于减小后面的负担。接着对化简结果进行求导,并且再次对结果进行化简,最后输出结果。

度量分析

可以看到,我的代码复杂度总体还是可以的。

唯一的问题在于 Parser 的平均循环复杂度较高,主要问题在于递归下降的过程中会有大量的路径依赖,或许可以通过为方法创建一个 map 解决问题,但是在这里感觉有点杀鸡用牛刀,故没有更改。

方法圈复杂度分析

方法 CogC ev(G) iv(G) v(G)
MainClass.main(String[]) 0.0 1.0 1.0 1.0
expression.Constant.Constant(BigInteger) 0.0 1.0 1.0 1.0
expression.Constant.derivative() 0.0 1.0 1.0 1.0
expression.Constant.display() 0.0 1.0 1.0 1.0
expression.Constant.getValue() 0.0 1.0 1.0 1.0
expression.Constant.hashCode() 0.0 1.0 1.0 1.0
expression.Constant.simplify() 0.0 1.0 1.0 1.0
expression.Expr.Expr(ArrayList<Pair<Term, BigInteger>>) 0.0 1.0 1.0 1.0
expression.Expr.consTerm(Term,BigInteger) 0.0 1.0 1.0 1.0
expression.Expr.derivative() 0.0 1.0 1.0 1.0
expression.Expr.getTerms() 0.0 1.0 1.0 1.0
expression.Expr.hashCode() 0.0 1.0 1.0 1.0
expression.Term.Term(ArrayList<Pair<Factor, BigInteger>>) 0.0 1.0 1.0 1.0
expression.Term.Term(BigInteger,HashMap<Factor, BigInteger>) 0.0 1.0 1.0 1.0
expression.Term.consPower(Factor,BigInteger) 0.0 1.0 1.0 1.0
expression.Term.getCoe() 0.0 1.0 1.0 1.0
expression.Term.getFactors() 0.0 1.0 1.0 1.0
expression.Term.hashCode() 0.0 1.0 1.0 1.0
expression.Var.derivative() 0.0 1.0 1.0 1.0
expression.Var.display() 0.0 1.0 1.0 1.0
expression.Var.equals(Object) 0.0 1.0 1.0 1.0
expression.Var.hashCode() 0.0 1.0 1.0 1.0
expression.Var.simplify() 0.0 1.0 1.0 1.0
pair.Pair.Pair(A,B) 0.0 1.0 1.0 1.0
pair.Pair.Pair(Entry<A, B>) 0.0 1.0 1.0 1.0
pair.Pair.getFirst() 0.0 1.0 1.0 1.0
pair.Pair.getSecond() 0.0 1.0 1.0 1.0
pair.Pair.hashCode() 0.0 1.0 1.0 1.0
parser.Parser.parse(String) 0.0 1.0 1.0 1.0
parser.Tokenizer.Tokenizer(String) 0.0 1.0 1.0 1.0
parser.Tokenizer.addToken(String,TokenType) 0.0 1.0 1.0 1.0
parser.Tokenizer.consumeToken() 0.0 1.0 1.0 1.0
parser.Tokenizer.hasToken() 0.0 1.0 1.0 1.0
parser.Tokenizer.isDeli(char) 0.0 1.0 1.0 1.0
parser.Tokenizer.isWhite(char) 0.0 1.0 1.0 1.0
expression.Term.derivative() 1.0 1.0 2.0 2.0
parser.Parser.consumeFactor() 2.0 2.0 2.0 2.0
parser.Parser.consumePowerPair() 2.0 2.0 2.0 2.0
parser.Tokenizer.getToken() 1.0 1.0 2.0 2.0
parser.Tokenizer.getTokenType() 1.0 1.0 2.0 2.0
expression.Constant.equals(Object) 2.0 3.0 1.0 3.0
expression.Expr.equals(Object) 2.0 3.0 1.0 3.0
expression.Expr.packTerm(Factor) 3.0 3.0 3.0 3.0
expression.Term.display() 4.0 1.0 2.0 3.0
expression.Term.equals(Object) 2.0 3.0 1.0 3.0
expression.Expr.simplify() 4.0 4.0 3.0 4.0
expression.Term.simplifyEntry(Entry<Factor, BigInteger>) 3.0 3.0 4.0 4.0
pair.Pair.equals(Object) 3.0 3.0 2.0 4.0
parser.Parser.consumeNumber() 3.0 1.0 4.0 4.0
parser.Tokenizer.getDeli(char) 1.0 4.0 1.0 4.0
parser.Parser.consumeTermPair() 4.0 1.0 5.0 5.0
expression.Term.simplify() 6.0 5.0 4.0 6.0
parser.Parser.consumeExpr() 7.0 4.0 6.0 6.0
expression.Expr.display() 9.0 2.0 4.0 7.0
parser.Tokenizer.getTokens(String) 17.0 3.0 10.0 12.0
Total 77.0 85.0 96.0 116.0
Average 1.4 1.5454545454545454 1.7454545454545454 2.109090909090909

可以发现,大部分方法圈复杂度较低,在可以接受的范围内。

出现问题的几个主要方法为 simplifyparse,考虑下一次改进。

性能分策略

本次作业比较简单,如果想要获得更多的性能分,可以从以下几个角度分析:

  • x**2 变为 x*x
  • 正号提前,即-1+x 变为 x-1
  • 合并同类项

类型拆包(性能分重点)

我自己造出来的名词,需要配合合并同类项使用。

在化简的过程中可能会遇到类型过度包装的问题。比如一个简单的数字,parse 的时候可能变成了一个 Expr(Term(Power(Constant(2), 1), 1)),即 \(1 * 2^1\)。显然这样是不利于化简的,因为 equalshashCode() 会判断这样的式子和 \(2\) 不等价。因此要进行“拆包”。

实际上类型拆包并没有化简什么,只是减少了类型的封装层次。

BUG 分析

第一次强测被 Hack 惨了 QAQ。

BUG 所在类 所在方法 原因
对于 2**2 的因子没拆开 Term display() 没看清题意

得益于 Stream 的高度抽象性,修复 BUG 以后,代码行数增加了两行,圈复杂度未增加。

Hack 策略

我使用的 Hack 策略是随机数据 + 定点爆破。其中定点爆破又包括自己想的易错样例阅读同学代码分析出的 BUG

由于第一单元的代码比较简单,所以随机数据的效果比较好。并且阅读代码比较快,因此 Hack 成绩比较理想。

重构感想

第一次作业我经过了大量的重构,主要是为了后面的作业进行铺垫。(虽然第二次又重构了)

我认为作业的难点在于“合并同类项”,并且为此想了很久,终于想出了一个合理的架构方法:在 ExprTerm 中,我用 HashSet 来存储儿子,而不用 List,因为 List 在计算哈希值时会考虑到元素的相对顺序问题,而 Set 则无关顺序,只要成员相同可以。为了使用 HashSet,我为每一个类定义 hashCodeequals 方法。这个方法直接由 IDEA 生成即可。

虽然花费了大量时间,但是最后重构出来的架构我认为是理想的,在可扩展性性能优化上比较优越。

心得体会与经验总结

在本单元,我学习到了许多技巧和经验,包括 Java 的使用、递归下降等。

除此之外,我还学到了重构的方法和策略。例如如果是小规模的重构,则逐步替换自己的方法,每次替换用一定的单元测试数据辅助(不会 JUnit,所以是手动测试(悲)),保证重构后的正确性。

第二次作业

简介

这次加入了简单三角函数和括号嵌套。

UML 类图

|- MainClass:主类
|- expression (package):表达式包
    |- Factor (abstract class):因子类
        |- Constant:常数类
        |- Var:变量类
        |- Sin:三角函数类 Sin
        |- Cos:三角函数类 Cos
        |- Term:项类
        |- Power:乘方类
        |- Expr:表达式类
|- parser (package):表达式解析包
    |- Parser:语法分析器
    |- Tokenizer:词法分析器
        |- TokenType (enum):词法单元类型

得益于第一次架构设计的合理性,本次在架构上没有大的变更,仅仅是增加了两个三角函数类。

并且考虑到 Pair 类型虽然方便,但是在语义表达上不明晰,我替换掉了它,并且换成了 Power 类。

度量分析

可以看到,本次的问题和第一次差不多,主要是 Parser 的问题。

除此之外,本次的 Expr 类的总循环复杂度也出现了问题。个人经过分析认为是 Expr 内调用方法关系太复杂,这一点将在第三次小重构时改善。

方法圈复杂度分析

方法 CogC ev(G) iv(G) v(G)
expression.Constant.Constant(BigInteger) 0.0 1.0 1.0 1.0
expression.Constant.derivative() 0.0 1.0 1.0 1.0
expression.Constant.display() 0.0 1.0 1.0 1.0
expression.Constant.getValue() 0.0 1.0 1.0 1.0
expression.Constant.hashCode() 0.0 1.0 1.0 1.0
expression.Constant.simplify() 0.0 1.0 1.0 1.0
expression.Cos.Cos(Factor) 0.0 1.0 1.0 1.0
expression.Cos.derivative() 0.0 1.0 1.0 1.0
expression.Cos.display() 0.0 1.0 1.0 1.0
expression.Cos.getContent() 0.0 1.0 1.0 1.0
expression.Cos.hashCode() 0.0 1.0 1.0 1.0
expression.Cos.simplify() 0.0 1.0 1.0 1.0
expression.Expr.Expr(ArrayList) 0.0 1.0 1.0 1.0
expression.Expr.Expr(HashSet) 0.0 1.0 1.0 1.0
expression.Expr.combine(ArrayList) 0.0 1.0 1.0 1.0
expression.Expr.derivative() 0.0 1.0 1.0 1.0
expression.Expr.getTerms() 0.0 1.0 1.0 1.0
expression.Expr.hashCode() 0.0 1.0 1.0 1.0
expression.Power.Power(Factor) 0.0 1.0 1.0 1.0
expression.Power.Power(Factor,BigInteger) 0.0 1.0 1.0 1.0
expression.Power.derivative() 0.0 1.0 1.0 1.0
expression.Power.getBase() 0.0 1.0 1.0 1.0
expression.Power.getExp() 0.0 1.0 1.0 1.0
expression.Power.hashCode() 0.0 1.0 1.0 1.0
expression.Sin.Sin(Factor) 0.0 1.0 1.0 1.0
expression.Sin.derivative() 0.0 1.0 1.0 1.0
expression.Sin.display() 0.0 1.0 1.0 1.0
expression.Sin.getContent() 0.0 1.0 1.0 1.0
expression.Sin.hashCode() 0.0 1.0 1.0 1.0
expression.Sin.simplify() 0.0 1.0 1.0 1.0
expression.Term.Term(BigInteger,ArrayList) 0.0 1.0 1.0 1.0
expression.Term.Term(BigInteger,HashSet) 0.0 1.0 1.0 1.0
expression.Term.Term(BigInteger,Power) 0.0 1.0 1.0 1.0
expression.Term.combine(ArrayList) 0.0 1.0 1.0 1.0
expression.Term.derivative() 0.0 1.0 1.0 1.0
expression.Term.getCoe() 0.0 1.0 1.0 1.0
expression.Term.getPowers() 0.0 1.0 1.0 1.0
expression.Term.hashCode() 0.0 1.0 1.0 1.0
expression.Var.derivative() 0.0 1.0 1.0 1.0
expression.Var.display() 0.0 1.0 1.0 1.0
expression.Var.equals(Object) 0.0 1.0 1.0 1.0
expression.Var.hashCode() 0.0 1.0 1.0 1.0
expression.Var.simplify() 0.0 1.0 1.0 1.0
parser.Parser.consumeCos() 0.0 1.0 1.0 1.0
parser.Parser.consumeSin() 0.0 1.0 1.0 1.0
parser.Parser.consumeX() 0.0 1.0 1.0 1.0
parser.Tokenizer.Tokenizer(String) 0.0 1.0 1.0 1.0
parser.Tokenizer.addToken(String,TokenType) 0.0 1.0 1.0 1.0
parser.Tokenizer.consumeToken() 0.0 1.0 1.0 1.0
parser.Tokenizer.hasToken() 0.0 1.0 1.0 1.0
parser.Tokenizer.isDeli(char) 0.0 1.0 1.0 1.0
parser.Tokenizer.isWhite(char) 0.0 1.0 1.0 1.0
MainClass.main(String[]) 1.0 1.0 2.0 2.0
expression.Expr.extractTrigo(HashSet,Power,HashMap<HashSet, BigInteger>) 2.0 1.0 2.0 2.0
expression.Power.pack(Factor) 1.0 1.0 1.0 2.0
parser.Parser.consumePower(Factor) 2.0 2.0 2.0 2.0
parser.Parser.parse(String) 1.0 2.0 1.0 2.0
parser.Tokenizer.getToken() 1.0 1.0 2.0 2.0
parser.Tokenizer.getTokenType() 1.0 1.0 2.0 2.0
expression.Constant.equals(Object) 2.0 3.0 1.0 3.0
expression.Cos.equals(Object) 3.0 3.0 2.0 4.0
expression.Expr.display() 4.0 2.0 2.0 4.0
expression.Expr.equals(Object) 3.0 3.0 2.0 4.0
expression.Sin.equals(Object) 3.0 3.0 2.0 4.0
expression.Term.display() 4.0 4.0 4.0 4.0
expression.Term.pack(Factor) 4.0 4.0 3.0 4.0
parser.Parser.consumeNumber() 3.0 1.0 4.0 4.0
parser.Tokenizer.getDeli(char) 1.0 4.0 1.0 4.0
expression.Power.display() 5.0 1.0 3.0 5.0
expression.Power.equals(Object) 4.0 3.0 3.0 5.0
expression.Term.equals(Object) 4.0 3.0 3.0 5.0
parser.Parser.consumeTerm(BigInteger) 4.0 1.0 5.0 5.0
parser.Parser.consumeFactor() 1.0 6.0 6.0 6.0
expression.Expr.simplify() 10.0 4.0 6.0 7.0
expression.Power.simplify() 7.0 5.0 7.0 7.0
parser.Parser.consumeExpr() 5.0 2.0 4.0 7.0
expression.Term.simplify() 10.0 3.0 7.0 8.0
expression.Expr.trigoCombine(HashSet) 21.0 1.0 9.0 13.0
parser.Tokenizer.getTokens(String) 24.0 3.0 13.0 17.0
Total 131.0 120.0 151.0 186.0
Average 1.6582278481012658 1.518987341772152 1.9113924050632911 2.3544303797468356

圈复杂度迅速上升……经过上次的重构,改掉了 simplify 的问题,将其抽象出了几个 helper 方法。

这次主要原因在于 Parser,递归下降导致圈复杂度上升。

性能分策略

除了第一次的策略外,本次我加了简单的三角函数化简,即对于模式 a sin^2 x + b cos^2 x + c 的化简。

我的思路是做这个优化有两种可能,要么是 sincos,要么是 cossin,这两个涵盖了所有情况。

性能分 trick

  • Power
    • ExprTerm 不能用乘方运算,所以如果 base instanceof Expr | Term 的话,我们需要输出时进行展开:
    • 如果 exp 为 1,那么不需要加上 **exp
    • 可以进行一个优化:x**2 -> x*2
    • 否则返回 base**exp
  • Term
    • 如果 powers 为空,直接返回系数
    • 如果系数为 1,返回 powerStr;如果系数为 -1,返回 -powerStr
    • 否则返回 (coe * power1 * power2 *...),这里之所以要加一对括号,是因为对于 sin((2*x)) 的情况
  • Expr
    • 如果 terms 为空,直接返回 0
    • 将所有项转换为字符串后,可以将正项放前面(比如 x-1-1+x 更好)
    • 如果开头是 + 那么去掉它
    • 返回 ([-]term1 [+-] term2 [+-] ...)

BUG 分析

本次强测互测均没有测出 BUG。

自己测试的时候出现过 x 不输出的情况,分析发现是自己漏判断了一种情况。

Hack 策略

Hack 策略还是随机数据 + 定点爆破。

分析同学代码发现 BUG 主要集中在括号输出上和解析。

例如对于输出 (x) 很多人会想到直接去掉两侧的括号,但是没有考虑到 (x+1)*(x+1),将其化简成 x+1)*(x+1,导致出现问题。

解析的问题主要出现在不合理地使用正则表达式,导致解析过慢或者解析出错。

重构感想

虽然这次架构看起来变化不大,但是实际上更改了很多内部的方法。

重构前后的一个显著优势在于降低了方法的圈复杂度。例如将部分循环操作和条件判断操作用 Stream 来化简,使用声明式编程的方式增加了代码的可读性,降低了 BUG 出现的概率。

心得体会与经验总结

随机测试非常重要!
随机测试非常重要!
随机测试非常重要!

本人通过课下的随机测试发现了自己的 BUG。随机测试不仅能够帮助自己发现问题,还能“友善”地帮助同学发现问题,可谓是一举两得。(<ゝω·)~☆

第三次作业

简介

本次作业增加了 Wrong Format 的判断以及嵌套因子。

UML 类图

得益于第二次的重构,本次的表达式架构只改了 4 行代码。除此之外加了一个异常类来判断 Wrong Format。

除此之外,我还在主类里面执行了括号展开判断,这也为后面度量分析爆炸埋下伏笔(悲)。

度量分析

可以发现,这次的 Parser 和 MainClass 的循环复杂度出新锅了。

经过分析,本人认为有两个问题:

  • 判断 Wrong Format 引入大量条件结构
  • 展开括号引入大量判断

针对二者,我提出了两个优化方向:

  • 重构异常处理措施
  • 将括号展开融入到 simplify 方法,降低循环复杂度

方法圈复杂度分析

方法 CogC ev(G) iv(G) v(G)
expression.Constant.Constant(BigInteger) 0.0 1.0 1.0 1.0
expression.Constant.derivative() 0.0 1.0 1.0 1.0
expression.Constant.display() 0.0 1.0 1.0 1.0
expression.Constant.getValue() 0.0 1.0 1.0 1.0
expression.Constant.hashCode() 0.0 1.0 1.0 1.0
expression.Constant.simplify() 0.0 1.0 1.0 1.0
expression.Cos.Cos(Factor) 0.0 1.0 1.0 1.0
expression.Cos.derivative() 0.0 1.0 1.0 1.0
expression.Cos.display() 0.0 1.0 1.0 1.0
expression.Cos.getContent() 0.0 1.0 1.0 1.0
expression.Cos.hashCode() 0.0 1.0 1.0 1.0
expression.Cos.simplify() 0.0 1.0 1.0 1.0
expression.Expr.Expr(ArrayList) 0.0 1.0 1.0 1.0
expression.Expr.Expr(HashSet) 0.0 1.0 1.0 1.0
expression.Expr.combine(ArrayList) 0.0 1.0 1.0 1.0
expression.Expr.derivative() 0.0 1.0 1.0 1.0
expression.Expr.getTerms() 0.0 1.0 1.0 1.0
expression.Expr.hashCode() 0.0 1.0 1.0 1.0
expression.Power.Power(Factor) 0.0 1.0 1.0 1.0
expression.Power.Power(Factor,BigInteger) 0.0 1.0 1.0 1.0
expression.Power.derivative() 0.0 1.0 1.0 1.0
expression.Power.getBase() 0.0 1.0 1.0 1.0
expression.Power.getExp() 0.0 1.0 1.0 1.0
expression.Power.hashCode() 0.0 1.0 1.0 1.0
expression.Sin.Sin(Factor) 0.0 1.0 1.0 1.0
expression.Sin.derivative() 0.0 1.0 1.0 1.0
expression.Sin.display() 0.0 1.0 1.0 1.0
expression.Sin.getContent() 0.0 1.0 1.0 1.0
expression.Sin.hashCode() 0.0 1.0 1.0 1.0
expression.Sin.simplify() 0.0 1.0 1.0 1.0
expression.Term.Term(BigInteger,ArrayList) 0.0 1.0 1.0 1.0
expression.Term.Term(BigInteger,HashSet) 0.0 1.0 1.0 1.0
expression.Term.Term(BigInteger,Power) 0.0 1.0 1.0 1.0
expression.Term.combine(ArrayList) 0.0 1.0 1.0 1.0
expression.Term.derivative() 0.0 1.0 1.0 1.0
expression.Term.getCoe() 0.0 1.0 1.0 1.0
expression.Term.getPowers() 0.0 1.0 1.0 1.0
expression.Term.hashCode() 0.0 1.0 1.0 1.0
expression.Var.derivative() 0.0 1.0 1.0 1.0
expression.Var.display() 0.0 1.0 1.0 1.0
expression.Var.equals(Object) 0.0 1.0 1.0 1.0
expression.Var.hashCode() 0.0 1.0 1.0 1.0
expression.Var.simplify() 0.0 1.0 1.0 1.0
parser.Parser.consumeCos() 0.0 1.0 1.0 1.0
parser.Parser.consumeSin() 0.0 1.0 1.0 1.0
parser.Parser.consumeX() 0.0 1.0 1.0 1.0
parser.Tokenizer.Tokenizer(String) 0.0 1.0 1.0 1.0
parser.Tokenizer.addToken(String,TokenType) 0.0 1.0 1.0 1.0
parser.Tokenizer.consumeToken() 0.0 1.0 1.0 1.0
parser.Tokenizer.hasToken() 0.0 1.0 1.0 1.0
parser.Tokenizer.isDeli(char) 0.0 1.0 1.0 1.0
parser.Tokenizer.isWhite(char) 0.0 1.0 1.0 1.0
MainClass.main(String[]) 1.0 1.0 2.0 2.0
expression.Expr.extractTrigo(HashSet,Power,HashMap<HashSet, BigInteger>) 2.0 1.0 2.0 2.0
expression.Power.pack(Factor) 1.0 1.0 1.0 2.0
parser.Parser.consumePower(Factor) 2.0 2.0 2.0 2.0
parser.Parser.parse(String) 1.0 2.0 1.0 2.0
parser.Tokenizer.getToken() 1.0 1.0 2.0 2.0
parser.Tokenizer.getTokenType() 1.0 1.0 2.0 2.0
expression.Constant.equals(Object) 2.0 3.0 1.0 3.0
expression.Cos.equals(Object) 3.0 3.0 2.0 4.0
expression.Expr.display() 4.0 2.0 2.0 4.0
expression.Expr.equals(Object) 3.0 3.0 2.0 4.0
expression.Sin.equals(Object) 3.0 3.0 2.0 4.0
expression.Term.display() 4.0 4.0 4.0 4.0
expression.Term.pack(Factor) 4.0 4.0 3.0 4.0
parser.Parser.consumeNumber() 3.0 1.0 4.0 4.0
parser.Tokenizer.getDeli(char) 1.0 4.0 1.0 4.0
expression.Power.display() 5.0 1.0 3.0 5.0
expression.Power.equals(Object) 4.0 3.0 3.0 5.0
expression.Term.equals(Object) 4.0 3.0 3.0 5.0
parser.Parser.consumeTerm(BigInteger) 4.0 1.0 5.0 5.0
parser.Parser.consumeFactor() 1.0 6.0 6.0 6.0
expression.Expr.simplify() 10.0 4.0 6.0 7.0
expression.Power.simplify() 7.0 5.0 7.0 7.0
parser.Parser.consumeExpr() 5.0 2.0 4.0 7.0
expression.Term.simplify() 10.0 3.0 7.0 8.0
expression.Expr.trigoCombine(HashSet) 21.0 1.0 9.0 13.0
parser.Tokenizer.getTokens(String) 24.0 3.0 13.0 17.0
Total 131.0 120.0 151.0 186.0
Average 1.6582278481012658 1.518987341772152 1.9113924050632911 2.3544303797468356

吸取上次的教训,这次更改了递归下降的实现方式,大幅减少了圈复杂度。

性能分策略

  • 很多很多三角优化:然而并没有写
  • 括号展开:重中之重!很有效

括号展开

我采用的策略是“要么全部不展开”,“要么全部展开”。

为了方便表达式化简,需要事先定义 Term * TermExpr * Expr 的运算。

括号展开主要是三个方面:

  • Power:对于 \((Y)^{\mathrm{exp}}\) 形式的表达式需要展开
  • Term:对于 \(C * A * B * (Y) * (Z)\),首先提取出非表达式部分将其转换成 Expr \((F) = (C * A * B)\),然后用乘法计算 \((F) * (Y) * (Z)\)
  • Expr:对于每一个 Term 进行展开,然后合并

需要注意的是对于每一个子因子展开后都要进行化简,避免出现类型过度包装。

BUG 分析

互测被 Hack 出一个 BUG。

BUG 所在类 所在方法 原因
sin(((x+1)*(x+1))) 展开成 sin((x+1)*(x+1)) Power display() 没有考虑周全

还是由于 Stream,修复 BUG 只让代码行数多了一行,并没有带来圈复杂度的增长。

发现了别人的两个 BUG:

  • 对于 ((((((((((((((0)))))))))))))) 化简太慢,TLE
  • 对于三角函数括号嵌套处理不正确

Hack 策略

随机爆破,以及利用前两次作业的数据测试。

我认为在第三次作业试图通过阅读代码发现 BUG 是不现实的,所以采用了有点偷懒的方式,最后确实发现了同学两个 BUG。

重构感想

第三次没有经过大的重构,只是加了一些性能优化以及错误处理。

心得体会与经验总结

经验总结:

  • 一定要认真做 Pre
  • 要用面向对象的思想思考架构
  • 要善用讨论区
  • 自动评测机和随机数据生成非常重要
  • 架构想得越早越好

心得总结:

本次作业最大的收获是对于 Java 的了解更加深入了,包括各种类库的使用方法,以及面向对象程序架构设计理念。当然,另一个重要的收获就是对于递归下降算法的学习。递归下降的理念非常简单,实现非常巧妙,而功能强大。由于利用了工厂方法的思想,其扩展性极强。

除此之外,我认为重构是这一单元的主题,一个好的架构可以起到事半功倍的效果。除此之外,评测机的编写以及数据的构造也是这门课程不可或缺的一个重要部分,他们不仅能快速找到自己的程序中早先被自己忽视的 BUG 以及自己编写代码时的疏漏,还能帮助同学一起改进它们程序(逃)。

本人初中的时候也开发类似于计算器的程序,利用的是栈解析输入,支持三角函数等。一直想要做类似于同类项合并或者表达式展开之类的代数系统,由此还了解到了清华的 Mathmu 项目,但是却当时并没有设计架构的能力,逐渐遗忘了自己的目标。这次的作业在某种程度上也是实现了我初中的计划吧(感慨)。

对课程的想法

经过第一单元,同学们对面向对象这门课程有了深刻的认识,架构水平也有了提高,但是我觉得第一单元的课程安排仍然有改进的余地,因此希望能够提出一些意见供参考。

例如有不少同学抱怨本单元压力很大,并且难点集中于 Parser 的写法,而非架构本身。因此我想是不是可以将 Parser 的部分提前到 Pre 的部分,让同学们能够提前预习相关的知识,以此来减轻做第一单元时的负担。这样既可以让同学们将注意力集中在架构上,也可以让大家轻松一些。与此同时,还可以增加一些架构的难度(例如加入除法(逃)),我认为这样更符合同学们对于这门课程的期望。

posted @ 2021-03-30 00:53  roife  阅读(416)  评论(1编辑  收藏  举报