BUAA OO UNIT1总结

第一次作业

在前面放上我个人博客的地址吧,欢迎大家来踩~

OO第一次作业总结
OO第二次作业总结
OO第三次作业总结

前言

本次 OO 作业经历过一次重构,而且重构的完成时间是在提交截止之后(哭)。所以强测寄的很惨,呜呜呜。(不过在房里刀的也很爽)

不过这份重构我个人是十分满意的。

特别感谢 sheeptoby 的帮助!

整体结构

类图如下所示:

类图

  • Expr:表达式
  • Term:项
  • Var:原子项
  • Lexer:词法分析器
  • Parser:语法分析器
  • Tool:工具类

注:图中省略了若干构造函数和获取类属性的接口。

对各类的分析

Lexer

本类作为词法解析器。peek() 方法可以取得当前解析得到的词汇。next() 方法会继续取得下一个词汇。当读取到句末时,继续使用 next() 将会读取到一个空格。

评价:中规中矩的词法解释器,基本是抄课程组的。

Expr

本类是表达式。我对表达式的定义如下:

  • 表达式由至少一个项连接而成。
  • 表达式本身不含符号,由各个项决定各自的符号。

只有一个属性:ArrayList<Term>,存储表达式内部的各个项。

具有 toString() 方法将表达式转换为字符串。

评价:或许应该将表达式乘法添加到其中。不过这样在运算过程中会涉及到一层额外的包装,可能有损性能。此外可以对表达式内部项进行排序,但是考虑到递归时可能带来的性能开销也作罢。

Term

本类是项。我对项的定义如下:

  • 项由至少一个原子项相乘得到。
  • 项本身默认的符号为正。在化简过程中符号根据项内部原子项符号改变。
  • 输出项时会输出项本身的符号。

项包含两个属性:ArrayList<Var>symbolarraylist 内存储实际的内容,symbol 内存储项本身的符号。

具有化简方法、设置符号方法、深拷贝方法、取得常数方法、判断是否相似方法。

评价:项的方法普遍比较复杂,这个类也是逻辑上最复杂的类。项的化简方法的复杂度相对也比较高。

Var

本类是原子项。我对原子项的定义如下:

  • 原子项要么是常数,要么是变量。
  • 原子项含有幂次和符号。

原子项包含的属性:type 用于决定原子项的类型(常数或变量),num 存储常数,character 存储变量,index 存储幂次,symbol 存储符号。

原子项具有加减的方法。(仅限于常数运算)

评价:写的时候脑子一抽写了很多不必要的重载构造方法。实际上第一次作业中只需要两个构造方法就够用了。本类在第一次作业中复杂度较低。

Parser

本类是语法分析类。进行语法分析工作。

  • parseExpr:对表达式进行语法分析。
  • parseTerm:对项进行语法分析。括号的解析也将在这一层中进行。
  • parseVar:分析原子项。

其中对表达式和项的语法分析都会返回一个项的集合。对表达式的分析返回项的集合的原因不言而喻。对项的语法分析返回一个项的集合的原因如下:

  1. 对括号的解析处在项这一级。
  2. 解析到括号时,由于括号内部是表达式,解析项会将这个括号展开,于是会得到一个项的集合。
  3. 在解析项的过程中各个项之间都是相乘的关系。

评价:核心解析、化简流程皆在本类中进行。对括号的解析和对基本项的解析都可以新增方法以降低原有方法的复杂度。

Tool

本类是工具类。包含了一些通用的工具:

  • 包含预处理和事后处理。
  • 包含对乘法方法的重载。
  • 包含表达式的化简方法。

之所以将乘法的重载和表达式的化简方法置于 Tool 内部是考虑到在 Parser 类中主要通过 ArrayList<Term> 的形式计算化简,所以需要在工具类中定义静态方法操作数组。

其中表达式化简的时机是在已经解析了表达式,返回表达式之前时。

评价:本类的方法全部是静态方法,供全局调用。重载了表达式与表达式、表达式与项之间的乘法。还做了必要的预处理和事后处理。预处理也可以说是化简流程中的一个精髓了。经过预处理后,无需考虑空格、连续符号的问题,对括号的解析流程也符合我自己定义的标准。

化简流程

预处理

预处理的流程:

  1. 去除所有空字符
  2. 将 “(” 替换成 “1*(”
  3. 将所有连续的正负号化简成一个符号
  4. 将 “*+” 化简成 “*”
  5. 将 “**” 化简成 “^”

经过预处理后,我们得到的字符串中没有任何的空白与连续的符号,并且乘方使用 “^” 表示后更加易于处理。

将括号前面添加 1* 是为了保证解析到括号时处于项的解析流程中。

使用 replaceAll() 正则替换就可以实现预处理。

解析

将表达式视作 表达式->项->原子项 的结构。即:

  • Expr = Term + Term
  • Term = Var * Var

对于符号,解析时首先将符号读取至原子项中,在化简过程中将符号归一到其所属的项中。

对于括号,整个解析过程是边解析边化简的。具体而言,在读取到括号时我会将括号内部视作一个表达式进行解析,返回经过化简后的 ArrayList<Term>ArrayList<Term> 与表达式在逻辑上是等价的)。

由于 ArrayList<Term> 与表达式在逻辑上是等价的,故 parseExpr()parseTerm() 的返回值都是 ArrayList<Term>。后者之所以返回数组是为了支持括号的解析。

处理括号

遇到括号时,在 praseTerm() 中递归调用 praseExpr()。在 praseExpr() 中将括号内的表达式分析、化简完毕再回溯到 praseTerm()(此时 parseTerm() 就会返回多个 Term)。

举例而言:对于 1+(x**2-1)*3,经过预处理后:1+1*(x**2-1)*3,在读取到 “(” 时处于 parseTerm() 的调用中。经调用 parseExpr() 后得到一个 ArrayList<Term>。之后接着读取处理后面的 *3,最后返回的 Term 数组中包含 3*x**2-3

处理乘方

“^” 可能出现在 “xyz)” 的后面。于是在 praseVar() 中和 praseTerm() 中相应的位置直接进行分析、展开(暴力循环即可)。我使用 Var 中的一个属性 index 来存储原子项(xyz 与数字,不过数字的乘方采取直接计算的方法)的乘方。

事后处理

事后处理( afterTreat )的时候使用正则替换进行简单的优化。包括:

  1. 将 “^” 展开为 “**”
  2. 将 “+1*” 化简为 “+”
  3. 将 “-1*” 化简为 “-”
  4. 去除首位的正号

复杂度分析

methodCogCev(G)iv(G)v(G)
Expr.addTerm(Term)0.01.01.01.0
Expr.addTerms(ArrayList<Term>)1.01.02.02.0
Expr.Expr()0.01.01.01.0
Expr.Expr(ArrayList<Term>)1.01.02.02.0
Expr.toString()1.01.02.02.0
Lexer.getNumber()2.01.03.03.0
Lexer.Lexer(String)0.01.01.01.0
Lexer.next()3.02.03.04.0
Lexer.peek()0.01.01.01.0
MainClass.main(String[])0.01.01.01.0
Parser.parseExpr()2.01.03.03.0
Parser.Parser(Lexer)0.01.01.01.0
Parser.parseTerm()19.01.09.09.0
Parser.parseVar()8.03.07.07.0
Term.add(Term)7.01.04.05.0
Term.addVar(Var)1.01.02.02.0
Term.addVars(ArrayList<Var>)2.02.02.03.0
Term.addVars(Term)1.01.02.02.0
Term.copy()0.01.01.01.0
Term.getConstant()3.03.03.03.0
Term.getSymbol()0.01.01.01.0
Term.setSymbol()1.01.02.02.0
Term.setSymbol(int)0.01.01.01.0
Term.similar(Term)9.03.05.07.0
Term.simplify()8.02.05.06.0
Term.Term()0.01.01.01.0
Term.Term(int)0.01.01.01.0
Term.Term(Var)0.01.01.01.0
Term.toString()3.02.02.04.0
Tool.afterTreat(String)1.01.02.02.0
Tool.exprSimplify(ArrayList<Term>)7.02.04.05.0
Tool.mul(ArrayList<Term>, ArrayList<Term>)3.01.03.03.0
Tool.mul(Term, Term)0.01.01.01.0
Tool.preTreat(String)2.01.05.05.0
Var.addAbs(Var)2.02.01.03.0
Var.copy()0.01.01.01.0
Var.equals(Var)3.02.03.05.0
Var.getCharacter()0.01.01.01.0
Var.getIndex()0.01.01.01.0
Var.getNum()1.01.02.02.0
Var.getSymbol()0.01.01.01.0
Var.getType()0.01.01.01.0
Var.setSymbol(int)0.01.01.01.0
Var.similar(Var)1.01.02.02.0
Var.simplify()6.04.04.06.0
Var.subAbs(Var)2.02.01.03.0
Var.toString()7.03.04.05.0
Var.Var(int)1.01.01.02.0
Var.Var(int, BigInteger, Character, int)0.01.01.01.0
Var.Var(int, BigInteger, Character, int, int)0.01.01.01.0
Var.Var(int, BigInteger, int, int)0.01.01.01.0
Var.Var(int, Character, int, int)0.01.01.01.0
Total108.071.0113.0132.0
Average2.0769230769230771.36538461538461542.1730769230769232.5384615384615383

复杂度分析

第一次作业类直接的耦合度不高,方法的复杂度也较低。其中对项的语法解析方法的复杂度较高。这是因为其中蕴含了对括号的分析。在之后的迭代过程中我将括号的解析拿到了新的方法中,降低了这一方法的复杂度。

bug 分析

重构前的代码有一个 bug,是会将 -x**0 解析成 +1。但是在交完中测之后直接把第一版代码抛弃了,故没有具体分析 bug 出现的原因(本文也没有对初版代码进行分析)。重构后的代码(即当前分析的代码)没有遇到 bug。此后的迭代工作也都是基于这版代码进行的。

hack 经验

因为交的代码比较寄所以去了 B 房…前两天还在忙着重构,最后只刀了一下午 + 一晚上。不过战绩还是可以的,总共中了 12 刀,经过修复后也拿了六分多的样子。

hack 的时候可以先手动构造一些易错的数据。跑评测机测出大量错误数据后要精缩数据到代价范围内,并且也方便同学修复 bug。此外要把刀过的数据记录下来,防止刀同质数据。

第二次作业

前言

相较于第一次作业,第二次作业进行迭代开发主要完成的功能有:

  1. 支持三角函数
  2. 支持自定义函数

括号嵌套在第一次作业中就已经实现。


整体架构

hw2 的类图如下所示:

类图

本次迭代中主要涉及到的工作有:

  • 新增了 Function 类,用于存储自定义函数的定义并作为 Parser 的属性参与解析过程。
  • Var 类中新增了属性 exprs,用于存储三角函数的表达式。
  • ExprTermVar 三个类新增了 equals() 方法。
  • Parser 类新增了 parseBracket() 用于专门解析括号,parseFunction() 用于专门解析函数。
  • Lexer 类增加了识别 Function 的方法:getFunction()
  • 对预处理函数进行了若干调整。

迭代思路

处理三角函数

首先,将三角函数作为一项新的属性添加到了原子项当中。

在视图中,三角函数内部包含一个表达式。判断两个三角函数是否相等即等价于判断其内部的表达式是否相等。于是对 ExprTermVar 三个类中添加了 equals() 方法。

解析三角函数等价于解析括号内部表达式。将解析得到的表达式添加至三角函数内即可。

并没有对三角函数作出很强的优化。仅做了类似 sin(0) 等于 0 这样的简单化简。

Function

本类是第二次作业中新增的类,用于处理、解析、调用函数。

本类主要包含构造函数和调用函数。

在构造过程中,将函数表达式进行格式化处理。举例而言:f(x,y)=sin(x**2)+y 经处理后,解析得到 sin(($0)**2)+($1) 存储进 functionexpr 中。

在调用方法,调用流程如下:

  1. 识别实参列表。
  2. $n 替换成对应的实参。
  3. 传回替换实参后的字符串。

自定义函数解析

Parser 类中新增了 parseFunction() 方法用于解析表达式中的函数。

在解析过程中,首先进行函数的调用得到替换实参后的字符串,之后对字符串进行表达式解析,再传回解析得到的表达式。

预处理

对预处理函数的主要处理有:将 cos1*(sin1*(f1*( 等替换成 cos(sin(1*f(

复杂度分析

methodCogCev(G)iv(G)v(G)
Expr.addTerm(Term)0.01.01.01.0
Expr.addTerms(ArrayList<Term>)1.01.02.02.0
Expr.equals(Expr)16.05.05.09.0
Expr.Expr()0.01.01.01.0
Expr.Expr(ArrayList<Term>)2.02.02.03.0
Expr.getTerms()0.01.01.01.0
Expr.toString()1.01.02.02.0
Function.call(String)9.01.06.07.0
Function.Function(String)1.01.02.02.0
Function.getExpr()0.01.01.01.0
Function.getState()0.01.01.01.0
Function.toString()0.01.01.01.0
Lexer.getFunction()5.01.02.04.0
Lexer.getNumber()2.01.03.03.0
Lexer.Lexer(String)0.01.01.01.0
Lexer.next()8.02.05.07.0
Lexer.peek()0.01.01.01.0
MainClass.main(String[])1.01.02.02.0
Parser.parseBracket(ArrayList<Term>)4.02.03.03.0
Parser.parseExpr()2.01.03.03.0
Parser.parseFunction(ArrayList<Term>, String)7.04.05.05.0
Parser.Parser(Lexer, ArrayList<Function>)0.01.01.01.0
Parser.parseTerm()13.01.010.010.0
Parser.parseVar()23.07.011.015.0
Term.add(Term)9.03.05.07.0
Term.addVar(Var)1.01.02.02.0
Term.addVars(ArrayList<Var>)2.02.02.03.0
Term.addVars(Term)1.01.02.02.0
Term.copy()0.01.01.01.0
Term.equals(Term)17.06.05.010.0
Term.getConstant()3.03.03.03.0
Term.getSymbol()0.01.01.01.0
Term.setSymbol()1.01.02.02.0
Term.setSymbol(int)0.01.01.01.0
Term.similar(Term)39.013.015.021.0
Term.simplify()23.05.013.014.0
Term.Term()0.01.01.01.0
Term.Term(int)0.01.01.01.0
Term.Term(Var)0.01.01.01.0
Term.toString()6.03.03.06.0
Tool.afterTreat(String)1.01.02.02.0
Tool.exprSimplify(ArrayList<Term>)8.02.05.06.0
Tool.mul(ArrayList<Term>, ArrayList<Term>)3.01.03.03.0
Tool.mul(Term, Term)0.01.01.01.0
Tool.preTreat(String)2.01.05.05.0
Tool.removeSpace(String)0.01.01.01.0
Var.addAbs(Var)2.02.01.03.0
Var.copy()0.01.01.01.0
Var.equals(Var)5.04.03.06.0
Var.getCharacter()0.01.01.01.0
Var.getExpr()0.01.01.01.0
Var.getIndex()0.01.01.01.0
Var.getNum()1.01.02.02.0
Var.getSymbol()0.01.01.01.0
Var.getType()0.01.01.01.0
Var.setSymbol(int)0.01.01.01.0
Var.simplify()8.04.05.08.0
Var.subAbs(Var)2.02.01.03.0
Var.toString()9.04.07.09.0
Var.Var(int)1.01.01.02.0
Var.Var(int, BigInteger, Character, int)0.01.01.01.0
Var.Var(int, BigInteger, Character, int, int)0.01.01.01.0
Var.Var(int, BigInteger, Character, int, int, ArrayList<Term>)0.01.01.01.0
Var.Var(int, BigInteger, int, int)0.01.01.01.0
Var.Var(int, Character, int, int)0.01.01.01.0
Total239.0121.0179.0225.0
Average3.67692307692307671.86153846153846162.7538461538461543.4615384615384617

复杂度分析

可以看到在添加了三角函数后 Term 类的 similar 方法复杂度直接爆炸了…这主要是因为对三角函数的判断逻辑比较复杂。此外对原子项的语法解析也因为三角函数的加入而变得稍显臃肿。考虑改进的话可以将对三角函数的语法解析提取到单独的类中。

bug 分析

第二次作业的构建过程中没有遇到什么特别的 bug。强测互测也都顺利通过。

总结

第二周的压力比第一周小了很多,仿佛一切都在步上正轨,令人欣慰。虽然一度为弱智 bug 所苦恼,不过跟第一周的坐牢比起来已经幸福太多了。并且由于第一周架构比较合理(貌似是),我在本次的迭代开发的过程中并未遇到什么困难,整体架构也几乎没有变动。唯一最大的困难就是我本人捉急的码力(悲)

第三次作业

前言

这是面向对象课程的第三次作业总结博客。在第二次作业上进行了一定的迭代开发。工作量相比第二次作业而言并不大。

相较于第二次作业,本次作业实现的功能有:实现求导算子。

因为只需要实现一个功能,从任务量和难度来讲都是三次作业中最简单的一次。

整体架构

经过迭代后第三次作业的类图如下所示:

类图

本次迭代中涉及的工作有:

  • ExprTermVar 新增了求导方法(Expr 的求导方法处于 Tool 类中)。
  • Parser 新增了求导算子语法分析方法。
  • 对自定义函数的构造方法进行了一定的调整。
  • 对预处理函数进行了一些调整

对预处理的调整

主要调整是将 dx 替换为了 1*dx,这样可以保证解析到求导算子时处在 Term 的解析过程中。

求导实现思路

在遇到求导算子 dx/y/z 时,对其管辖的表达式进行语法分析。分析结束后对得到的表达式调用表达式求导函数得到求导后的表达式,后续处理与括号解析相同。

对于出现在自定义函数中的求导算子,我的处理是对自定义函数的表达式调用表达式语法分析,对表达式进行展开、化简操作,再进行形参替换。

Expr

对于 Expr 就是对其中每个 Term 都进行求导。采取循环即可,在循环过程中对每一项都进行求导。此3方法位于工具类 Tool 中。

Term

对于 Term 的求导主要考虑乘法法则。考虑到 (f(x)*g(x)*h(x))‘=f(x)’*(g(x)*h(x))+f(x)*(g(x)*h(x))'=…,采用递归的方法解析。伪代码如下所示:

ArrayList<Term> termDerivative
    new terms
    if this.vars.size == 1                  // 当只剩下一项的时候
        add vars[0].derivative to terms     // 返回原子项的求导结果
        return terms
    else 
        add mul(this.vars[0].derivative, this.vars[1,]) to terms // 前导后不导
        add mul(this.vars[0], this.vars[1,].derivative) to terms // 前不导后导
        return terms 

Var

对于 Var 的求导,分为三种情况:

  1. 常数求导返回零
  2. x n x^n xn 返回 n ∗ x n − 1 n*x^{n-1} nxn1
  3. 三角函数返回三角函数求导后的值。

其中对三角函数的求导举例:
( s i n n ( E x p r ) ) ′ = n ∗ s i n n − 1 ( E x p r ) ∗ c o s ( E x p r ) ∗ ( E x p r ) ′ (sin^n(Expr))'=n*sin^{n-1}(Expr)*cos(Expr)*(Expr)' (sinn(Expr))=nsinn1(Expr)cos(Expr)(Expr)

类复杂度分析

methodCgoCevivv
Expr.equals(Expr)16559
Expr.Expr()0111
Expr.Expr(ArrayList)2223
Expr.getTerms()0111
Expr.toString()6355
Function.call(String)9167
Function.Function(String, ArrayList)1122
Function.getState()0111
Function.toString()0111
Lexer.getFunction()5124
Lexer.getNumber()2133
Lexer.Lexer(String)0111
Lexer.next()9268
Lexer.peek()0111
MainClass.main(String[])1122
Parser.cket(ArrayList)4233
Parser.parseDerivation(ArrayList)4233
Parser.parseExpr()2133
Parser.parseFunction(ArrayList, String)7455
Parser.Parser(Lexer, ArrayList)0111
Parser.parseTerm()1411111
Parser.parseVar()2371115
Term.add(Term)9357
Term.addVar(Var)1122
Term.addVars(Term)1122
Term.copy()0111
Term.equals(Term)176510
Term.getConstant()3333
Term.getSymbol()0111
Term.setSymbol()1122
Term.setSymbol(int)0111
Term.similar(Term)39131521
Term.simplify()2351314
Term.Term()0111
Term.Term(int)0111
Term.Term(Var)0111
Term.termDerivative(Character)6344
Term.toString()6336
Tool.afterTreat(String)3166
Tool.exprDerivative(ArrayList, Character)3133
Tool.exprSimplify(ArrayList)8256
Tool.mul(ArrayList, ArrayList)3133
Tool.mul(ArrayList, Term)1122
Tool.mul(Term, Term)0111
Tool.preTreat(String)2155
Tool.removeSpace(String)0111
Var.addAbs(Var)2213
Var.copy()0111
Var.equals(Var)5436
Var.getCharacter()0111
Var.getExpr()0111
Var.getIndex()0111
Var.getNum()1122
Var.getSymbol()0111
Var.getType()0111
Var.setSymbol(int)0111
Var.simplify()8458
Var.subAbs(Var)2213
Var.toString()116710
Var.Var(int)1112
Var.Var(int, BigInteger, Character, int)0111
Var.Var(int, BigInteger, Character, int, int)0111
Var.Var(int, BigInteger, Character, int, int, ArrayList)0111
Var.Var(int, BigInteger, int, int)0111
Var.Var(int, Character, int, int)0111
Var.Var(int, int, int, ArrayList)0111
Var.varDerivative(Character)15368
Total276131201249
Average4.1194029851.95522388133.71641791

复杂度分析

本次迭代后复杂度并没有额外的增加。整体上基本与第二次作业相同。

bug 分析

本次作业并没有发现 bug

hack 策略

整体上 hack 以跑评测机为主,手动构造数据为辅。

值得一提的是在互测过程中成功刀到了 TLEMLE。出刀的过程中包含了各种卡着代价上限的微调…结果是成功爆了一个人的时间与另一个人的堆,给我留下了珍贵的回忆。

数据分享:

// 干爆了时间
0
sin(sin(sin(sin(sin(sin(sin(sin(sin(x)))))))))**3

// 干爆了空间
0
((dx((x**2)))**3)**4

总结

拜整体还算是优良的架构所赐,第三次作业的难度可谓是三次作业中最简单的。经验是在编程过程中,优良的架构设计是很重要的。在写出屎山代码时要勇于重构,不要尝试在屎山上添砖加瓦。

因为没有参加 OOpre,所以在写第一次作业的过程中遇到了莫大的困难,当时的心态也受到了莫大的影响。很希望课程组以后会改进这一点,要么不要取消寒假的 OOpre,要么把 OOpre 课程变成大二上的必修课。

另外是互测刀的很爽(逃),但是个人感觉代价的设定有点太严苛了,过于严苛的代价设定让本来是比较合理的 hack 都无法通过合法性检验,比较影响刀人体验。

第一单元的学习还是很有遗憾的,比如第一次作业强测直接寄的经历呜呜呜。

特别感谢 Tobysheep 对我学习的帮助。

第一单元总结

经过本单元的训练,基本已经熟练掌握了 Java 的语法,并且学习了使用递归下降的方法分析语法。

互测的过程很有趣,孩子刀人刀的很开心(逃)

说起来第一周差点没写出来的时候真给孩子干emo了,太悲伤了呜呜呜。

不过经过这一个月的训练自我感觉算法的能力和码力都有所提升。在写完之后回顾感觉还是蛮开心的。但是第一次作业寄的好悲伤(念念不忘)

在大规模代码的过程中感受最深的一点是,设计好代码的架构远远比实际写代码的过程更重要。应当将更多的时间放在思考、分析代码的架构上,将更少的时间放在写代码上。尽量使用手绘项目结构、手写伪代码的方式理清思路、解决代码中隐含的问题,让码代码的过程变成照着蓝图施工的过程。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值