OO第一单元总结
前言
复杂度分析使用IDEA的 MetricsReloaded
插件
代码规模分析利用IDEA的 static
插件
UML
类图使用IDEA绘图,再自行调整
任务概览
作业要求
- 作业1:根据形式化定义解析输入的表达式,输出恒等变形展开括号后的表达式。
- 作业2:形式化定义加入指数函数和自定义函数。
- 作业3:支持自定义函数嵌套并加入求导算子。
第一次作业
第一次作业为对包含幂函数与常数的表达式进行化简,涉及相对简单的嵌套,UML类图如下所示。
架构分析
通过分析 Expr
与 Term
的结构。我们不难发现,一个基本的 Expr
可以看成又一个个基元构成,即
$$
Expr = \sum a \times x ^ b
$$
数据存储上,依照形式化表达设计为 Expr->Term->Factor
。Parser
和 Lexer
作为工具类,分别承担句法分析和词法分析的功能。
值得注意的是,架构中事实上没有区分 Num
和 Power
,而是将其统一为 Unit
类,存储一个形如$a \times x ^ b$的因子。
在读取和存储过程中,Parser
和 Lexer
合作,构建了一棵多叉表达式树。
Factor
接口为因子类的公共接口,所有的因子都实现了这个接口,所有的非 Unit
类通过 Poly
,即多项式类进行运算,最后通过 Poly
类调用 Unit
类的 toString
方法实现输出。
下图展示了这种读取和存储的过程的一个例子:
具体而言,获得答案的计算过程是和读取解析相解耦合的。在调用顶层 Expr
对象的 extend()
方法后,逐层向下调用 extend()
方法,得到展开式。这其中,得益于 Unit
类的统一性,每一层向上返回的都是一个 Unit
对象的集合,达到了形式上和接口的统一。
复杂度分析
本次作业部分方法复杂度如下图,其余方法复杂度较低,未在图中体现。
可以看到,Parser
中的 parseFactor()
方法复杂度较高,分析原因知,设计该方法时对于常数和幂函数的解析都全部列在其中,没有分别抽象为分别的方法;Unit
中的 toString()
方法复杂度较高,为了优化输出的长度导致特判较多,复杂度较高。
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Parser.parseFactor() | 14 | 1 | 8 | 8 |
Parser.parseExpr() | 8 | 1 | 6 | 6 |
expr.Unit.toString() | 7 | 1 | 6 | 7 |
Lexer.next() | 6 | 2 | 4 | 6 |
expr.Expr.extend() | 6 | 2 | 6 | 6 |
expr.Poly.toString() | 5 | 4 | 3 | 5 |
expr.Term.extend() | 5 | 1 | 4 | 4 |
Parser.parseTerm() | 4 | 1 | 4 | 4 |
expr.Poly.multiUnit(Unit) | 4 | 1 | 3 | 3 |
expr.Poly.Merge() | 3 | 1 | 3 | 3 |
expr.Poly.adjustUnit() | 3 | 3 | 2 | 3 |
expr.Poly.multiExpr(Poly) | 3 | 1 | 3 | 3 |
Lexer.getNumber() | 2 | 1 | 3 | 3 |
Parser.getExp() | 1 | 1 | 2 | 2 |
Processor.treatPlusMinus() | 1 | 1 | 2 | 2 |
expr.Poly.Negate() | 1 | 1 | 2 | 2 |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.peek() | 0 | 1 | 1 | 1 |
Main.main(String[]) | 0 | 1 | 1 | 1 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Processor.adjustSign() | 0 | 1 | 1 | 1 |
Processor.consumeBlank() | 0 | 1 | 1 | 1 |
Processor.getString(String) | 0 | 1 | 1 | 1 |
Processor.newInput(String) | 0 | 1 | 1 | 1 |
Processor.outputString() | 0 | 1 | 1 | 1 |
Processor.replaceDoubleStar() | 0 | 1 | 1 | 1 |
expr.Expr.Negate() | 0 | 1 | 1 | 1 |
expr.Expr.addTerm(Term) | 0 | 1 | 1 | 1 |
expr.Expr.equals(Expr) | 0 | 1 | 1 | 1 |
expr.Expr.setExp(BigInteger) | 0 | 1 | 1 | 1 |
expr.Poly.Poly() | 0 | 1 | 1 | 1 |
expr.Poly.add(Poly) | 0 | 1 | 1 | 1 |
expr.Poly.createOne() | 0 | 1 | 1 | 1 |
expr.Poly.unitMulti(Unit, Unit) | 0 | 1 | 1 | 1 |
expr.Term.Negate() | 0 | 1 | 1 | 1 |
expr.Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
expr.Unit.Negate() | 0 | 1 | 1 | 1 |
expr.Unit.Unit(BigInteger) | 0 | 1 | 1 | 1 |
expr.Unit.Unit(Type) | 0 | 1 | 1 | 1 |
expr.Unit.canMerge(Unit) | 0 | 1 | 1 | 1 |
expr.Unit.compareTo(Unit) | 0 | 1 | 1 | 1 |
expr.Unit.getCoff() | 0 | 1 | 1 | 1 |
expr.Unit.getExp() | 0 | 1 | 1 | 1 |
expr.Unit.getType() | 0 | 1 | 1 | 1 |
expr.Unit.merge(Unit) | 0 | 1 | 1 | 1 |
expr.Unit.multiUnit(Unit) | 0 | 1 | 1 | 1 |
expr.Unit.setCoff(BigInteger) | 0 | 1 | 1 | 1 |
expr.Unit.setExp(BigInteger) | 0 | 1 | 1 | 1 |
Bug分析
本次作业没有出现bug。
在对room内其他同学互测时,发现两个bug:一个是没有处理$0$的输出;另一个是没有妥善处理表达式里的 \t
和空格。
同时,我在此次作业中设计并实现了一个自动化评测机,这对检验程序的正确性有不小的帮助,也帮助我实现了hack,详细的分析将在评测部分介绍。
总结
“工欲善其事,必先利其器”
在写本次作业之初,我设计过很多架构,在设计阶段就推倒重来若干次。尽管在Pre中的冒险者游戏中已经我初步领会了面向对象的思想,但是面对较为抽象的表达式解析仍然显得捉襟见肘。
尽管如此,得益于 training
部分提供的 Parser
和 Lexer
思路,我最终得以确定这个较为面向对象的设计。但是,这个架构依然存在相当的不足,在后面的迭代开发介绍中将会着重介绍。
第二次作业
相比于第一次作业,第二次作业新增了自定义函数与指数函数相关的内容,同时支持嵌套括号的解析。
相比第一次作业,增加了 Func
类处理自定义函数,同时增加了 Exp
类处理指数函数,UML类图如下:
架构分析
相较于上一次作业以及未来的第三次作业,本次作业的工作量无疑是最大的,也难怪纪一鹏老师说这次作业很有可能会是本学期OO的难度巅峰。
对于本次作业的两个新增要求,我先完成了自定义函数的实现,再完成了指数函数的部分。
自定义函数部分
自定义函数分为两个部分,一是读入,二是调用。
有关读入,由于函数定义式依然符合我们的多项式语意,且定义式内不允许循环定义,故而直接沿用上一次的 Lexer
和 Parser
来解析。
有关调用,我在读入表达式时,就建立了参数与实参的Map,在解析时利用Map中的Map.Entry接口进行替换,效果不错,但是相较正则表达式替换的方法,虽然正确性上有保障,但是时间复杂度较高。
指数函数部分
通过分析本次式子可知,本次基元有所更新
$$
Expr = \sum a \times x ^ b \times exp(Factor) ^ c
$$
由于指数函数的特殊性,故我直接在初始化指数函数时直接将基元变形成如下式子,便于后续计算:
$$
Expr = \sum a \times x ^ b \times exp(Factor \times c)
$$
同样,我利用Poly类的 toString
方法和String类的equals方法判断exp内的Factor是否相等,来进行后续合并。
复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Term.replace(HashMap<Type, Factor>) | 30 | 1 | 8 | 8 |
Parser.parseFactor() | 19 | 1 | 12 | 12 |
Unit.isSimple() | 11 | 7 | 9 | 15 |
Unit.toString() | 11 | 1 | 9 | 10 |
Unit.setExp(BigInteger) | 10 | 1 | 4 | 8 |
Lexer.next() | 9 | 2 | 5 | 7 |
Parser.parseExpr() | 8 | 1 | 6 | 6 |
Unit.compareTo(Unit) | 8 | 7 | 7 | 8 |
Exp.ifOne() | 7 | 5 | 2 | 5 |
Expr.extend() | 6 | 2 | 6 | 6 |
Poly.Merge() | 6 | 1 | 5 | 5 |
Func.invoke(List <Factor> ) |
5 | 1 | 2 | 4 |
Poly.toString() | 5 | 4 | 3 | 5 |
Term.extend() | 5 | 1 | 4 | 4 |
Parser.parseTerm() | 4 | 1 | 4 | 4 |
Poly.multiUnit(Unit) | 4 | 1 | 3 | 3 |
Poly.unitMulti(Unit, Unit) | 4 | 1 | 5 | 5 |
Unit.multiUnit(Unit) | 4 | 1 | 4 | 5 |
Unit.wipeExp() | 4 | 4 | 1 | 5 |
Exp.Exp(Factor) | 3 | 1 | 3 | 4 |
Exp.toString() | 3 | 1 | 3 | 3 |
Poly.adjustUnit() | 3 | 3 | 2 | 3 |
Poly.multiExpr(Poly) | 3 | 1 | 3 | 3 |
Unit.Unit(Type) | 3 | 1 | 1 | 4 |
Unit.replace(HashMap<Type, Factor>) | 3 | 3 | 3 | 4 |
Exp.replace(HashMap<Type, Factor>) | 2 | 1 | 2 | 2 |
Lexer.getNumber() | 2 | 1 | 3 | 3 |
Parser.parseFuncDef() | 2 | 1 | 3 | 3 |
Poly.isSimple() | 2 | 3 | 1 | 3 |
Unit.clone() | 2 | 1 | 3 | 3 |
Expr.clone() | 1 | 1 | 2 | 2 |
Expr.pow(BigInteger) | 1 | 1 | 2 | 2 |
Expr.replace(HashMap<Type, Factor>) | 1 | 1 | 2 | 2 |
Main.main(String[]) | 1 | 1 | 2 | 2 |
Parser.getExp() | 1 | 1 | 2 | 2 |
Parser.parseFunc() | 1 | 1 | 2 | 2 |
Poly.Negate() | 1 | 1 | 2 | 2 |
Poly.clone() | 1 | 1 | 2 | 2 |
Poly.pow(BigInteger) | 1 | 1 | 2 | 2 |
Processor.treatPlusMinus() | 1 | 1 | 2 | 2 |
Term.clone() | 1 | 1 | 2 | 2 |
Exp.clone() | 0 | 1 | 1 | 1 |
Exp.compareTo(Exp) | 0 | 1 | 1 | 1 |
Exp.equals(Exp) | 0 | 1 | 1 | 1 |
Exp.getExponent() | 0 | 1 | 1 | 1 |
Exp.isSimple() | 0 | 1 | 1 | 1 |
Exp.multExp(Exp) | 0 | 1 | 1 | 1 |
Exp.pow(BigInteger) | 0 | 1 | 1 | 1 |
Exp.setExponent(BigInteger) | 0 | 1 | 1 | 1 |
Expr.Negate() | 0 | 1 | 1 | 1 |
Expr.addExpr(Expr) | 0 | 1 | 1 | 1 |
Expr.addTerm(Term) | 0 | 1 | 1 | 1 |
Expr.equals(Expr) | 0 | 1 | 1 | 1 |
Expr.setExp(BigInteger) | 0 | 1 | 1 | 1 |
Func.Func(String, Character[]) | 0 | 1 | 1 | 1 |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.peek() | 0 | 1 | 1 | 1 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Poly.Poly() | 0 | 1 | 1 | 1 |
Poly.add(Poly) | 0 | 1 | 1 | 1 |
Poly.createOne() | 0 | 1 | 1 | 1 |
Poly.isEmpty() | 0 | 1 | 1 | 1 |
Processor.adjustSign() | 0 | 1 | 1 | 1 |
Processor.consumeBlank() | 0 | 1 | 1 | 1 |
Processor.getString(String) | 0 | 1 | 1 | 1 |
Processor.newInput(String) | 0 | 1 | 1 | 1 |
Processor.outputString() | 0 | 1 | 1 | 1 |
Processor.replaceDoubleStar() | 0 | 1 | 1 | 1 |
Term.Negate() | 0 | 1 | 1 | 1 |
Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
Term.pow(BigInteger) | 0 | 1 | 1 | 1 |
Unit.Negate() | 0 | 1 | 1 | 1 |
Unit.Unit(BigInteger) | 0 | 1 | 1 | 1 |
Unit.Unit(Type, Factor) | 0 | 1 | 1 | 1 |
Unit.canMerge(Unit) | 0 | 1 | 1 | 1 |
Unit.getCoff() | 0 | 1 | 1 | 1 |
Unit.getExp() | 0 | 1 | 1 | 1 |
Unit.getExpFunc() | 0 | 1 | 1 | 1 |
Unit.getType() | 0 | 1 | 1 | 1 |
Unit.merge(Unit) | 0 | 1 | 1 | 1 |
Unit.pow(BigInteger) | 0 | 1 | 1 | 1 |
Unit.setCoff(BigInteger) | 0 | 1 | 1 | 1 |
Unit.setExpFunc(Exp) | 0 | 1 | 1 | 1 |
Unit.setType(Type) | 0 | 1 | 1 | 1 |
可以看出,本次架构在Term类中实现自定义函数的替换部分的 replace
方法复杂度极高,在Debug过程中,也同样是这个方法耗去了我大量的时间,可见越复杂,越易错!!!
Bug分析
本次作业没有出现bug。
在对room内其他同学互测时,发现两个bug:一个是对于exp的括号化简出错;另一个是自定义函数中exp函数替换出错。
在本次hack过程中,我着重查看了同屋同学代码架构的复杂程度进行了分析,对于明显架构复杂的代码进行hack,都得到了成功。
具体的hack方法则是利用cxc同学的评测机,对于可疑代码进行评测,得到错误输出后针对数据进行化简使其符合互测数据限制,最终hack成功。
总结
第二次作业是我在第一单元中花费时间最长的一次,尤其是指数函数的引入与化简合并,在Debug时给我造成了不小的困难,由于在完成本次作业时流感发烧,因此我在优化方面仅仅做了合并同类项和去exp函数的多余括号,但性能分上取得了 97.5
分,可以说是十分侥幸的。同时,由于储存基元时采用了ArrayList,因此在合并同类项与Unit运算时被迫使用深克隆,而由于身体状态不佳,处于正确性考虑,我大量使用克隆,这也使我的程序运行时间大大增长。
第三次作业
本次作业引入了求导因子。本以为是最痛苦的一集,没想到成了最速的一集。UML类图如下所示:
架构分析
与第二次作业相比,本次作业架构几乎没有发生变化,没有引入新的类。对于求导操作,与求字符串相似,在每一个相关的类中引入getDerivative()方法,返回Poly多项式类。对于带函数的求导,采用将自定义函数完全展开后再进行求导的方法。
复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Term.replace(HashMap<Type, Factor>) | 30 | 1 | 8 | 8 |
Parser.parseFactor() | 20 | 1 | 13 | 13 |
Unit.isSimple() | 11 | 7 | 9 | 15 |
Unit.toString() | 11 | 1 | 9 | 10 |
Lexer.next() | 10 | 2 | 6 | 8 |
Unit.setExp(BigInteger) | 10 | 1 | 4 | 8 |
Parser.parseExpr() | 8 | 1 | 6 | 6 |
Unit.compareTo(Unit) | 8 | 7 | 7 | 8 |
Exp.ifOne() | 7 | 5 | 2 | 5 |
Expr.extend() | 6 | 2 | 6 | 6 |
Poly.Merge() | 6 | 1 | 5 | 5 |
Func.invoke(List <Factor> ) |
5 | 1 | 2 | 4 |
Main.main(String[]) | 5 | 3 | 4 | 5 |
Poly.toString() | 5 | 4 | 3 | 5 |
Term.extend() | 5 | 1 | 4 | 4 |
Parser.parseTerm() | 4 | 1 | 4 | 4 |
Poly.multiUnit(Unit) | 4 | 1 | 3 | 3 |
Unit.multiUnit(Unit) | 4 | 1 | 4 | 5 |
Unit.wipeExp() | 4 | 4 | 1 | 5 |
Exp.Exp(Factor) | 3 | 1 | 3 | 4 |
Exp.toString() | 3 | 1 | 3 | 3 |
Parser.parseDerivative() | 3 | 1 | 4 | 4 |
Poly.adjustUnit() | 3 | 3 | 2 | 3 |
Poly.compareTo(Poly) | 3 | 3 | 3 | 3 |
Poly.multiExpr(Poly) | 3 | 1 | 3 | 3 |
Unit.Unit(Type) | 3 | 1 | 1 | 4 |
Unit.replace(HashMap<Type, Factor>) | 3 | 3 | 3 | 4 |
Exp.clone() | 2 | 1 | 1 | 2 |
Exp.replace(HashMap<Type, Factor>) | 2 | 1 | 2 | 2 |
Lexer.getNumber() | 2 | 1 | 3 | 3 |
Parser.parseFuncDef() | 2 | 1 | 3 | 3 |
Poly.isSimple() | 2 | 3 | 1 | 3 |
Unit.clone() | 2 | 1 | 3 | 3 |
Unit.getDerivative(Type) | 2 | 1 | 3 | 3 |
Exp.getDerivative(Type) | 1 | 2 | 1 | 2 |
Expr.clone() | 1 | 1 | 2 | 2 |
Expr.pow(BigInteger) | 1 | 1 | 2 | 2 |
Expr.replace(HashMap<Type, Factor>) | 1 | 1 | 2 | 2 |
Parser.getExp() | 1 | 1 | 2 | 2 |
Parser.parseFunc() | 1 | 1 | 2 | 2 |
Poly.Negate() | 1 | 1 | 2 | 2 |
Poly.clone() | 1 | 1 | 2 | 2 |
Poly.getDerivative(Type) | 1 | 1 | 2 | 2 |
Poly.pow(BigInteger) | 1 | 1 | 2 | 2 |
Poly.toTerms() | 1 | 1 | 2 | 2 |
Processor.treatPlusMinus() | 1 | 1 | 2 | 2 |
Term.clone() | 1 | 1 | 2 | 2 |
Exp.compareTo(Exp) | 0 | 1 | 1 | 1 |
Exp.equals(Exp) | 0 | 1 | 1 | 1 |
Exp.getExponent() | 0 | 1 | 1 | 1 |
Exp.isSimple() | 0 | 1 | 1 | 1 |
Exp.multExp(Exp) | 0 | 1 | 1 | 1 |
Exp.pow(BigInteger) | 0 | 1 | 1 | 1 |
Exp.setExponent(BigInteger) | 0 | 1 | 1 | 1 |
Expr.Expr() | 0 | 1 | 1 | 1 |
Expr.Expr(Poly) | 0 | 1 | 1 | 1 |
Expr.Negate() | 0 | 1 | 1 | 1 |
Expr.addExpr(Expr) | 0 | 1 | 1 | 1 |
Expr.addTerm(Term) | 0 | 1 | 1 | 1 |
Expr.equals(Expr) | 0 | 1 | 1 | 1 |
Expr.getDerivative(Type) | 0 | 1 | 1 | 1 |
Expr.setExp(BigInteger) | 0 | 1 | 1 | 1 |
Func.Func(String, Character[]) | 0 | 1 | 1 | 1 |
Lexer.Lexer(String) | 0 | 1 | 1 | 1 |
Lexer.peek() | 0 | 1 | 1 | 1 |
Parser.Parser(Lexer) | 0 | 1 | 1 | 1 |
Poly.Poly() | 0 | 1 | 1 | 1 |
Poly.add(Poly) | 0 | 1 | 1 | 1 |
Poly.addUnit(Unit) | 0 | 1 | 1 | 1 |
Poly.createOne() | 0 | 1 | 1 | 1 |
Poly.isEmpty() | 0 | 1 | 1 | 1 |
Poly.unitMulti(Unit, Unit) | 0 | 1 | 1 | 1 |
Processor.adjustSign() | 0 | 1 | 1 | 1 |
Processor.consumeBlank() | 0 | 1 | 1 | 1 |
Processor.getString(String) | 0 | 1 | 1 | 1 |
Processor.newInput(String) | 0 | 1 | 1 | 1 |
Processor.outputString() | 0 | 1 | 1 | 1 |
Processor.replaceDoubleStar() | 0 | 1 | 1 | 1 |
Term.Negate() | 0 | 1 | 1 | 1 |
Term.addFactor(Factor) | 0 | 1 | 1 | 1 |
Term.pow(BigInteger) | 0 | 1 | 1 | 1 |
Unit.Negate() | 0 | 1 | 1 | 1 |
Unit.Unit(BigInteger) | 0 | 1 | 1 | 1 |
Unit.Unit(Type, Factor) | 0 | 1 | 1 | 1 |
Unit.canMerge(Unit) | 0 | 1 | 1 | 1 |
Unit.getCoff() | 0 | 1 | 1 | 1 |
Unit.getExp() | 0 | 1 | 1 | 1 |
Unit.getExpFunc() | 0 | 1 | 1 | 1 |
Unit.getType() | 0 | 1 | 1 | 1 |
Unit.merge(Unit) | 0 | 1 | 1 | 1 |
Unit.pow(BigInteger) | 0 | 1 | 1 | 1 |
Unit.setCoff(BigInteger) | 0 | 1 | 1 | 1 |
Unit.setExpFunc(Exp) | 0 | 1 | 1 | 1 |
Unit.setType(Type) | 0 | 1 | 1 | 1 |
Unit.simpleClone() | 0 | 1 | 1 | 1 |
与第二次作业中复杂度较高的方法相同,原因一致。
Bug分析
本次作业在强测中未出现Bug,但是在互测中由于TLE中刀,原因正如我在第二次作业总结中写的那样——无脑克隆,而克隆方法本身需要递归,导致最终超时,这也是由于我个人的侥幸心理,认为互测数据不强,在明知有超时风险时,选择写OS作业,未进行优化。虽然强测侥幸过关,但是在互测中还是漏了马脚,果然还是这个道理。
She was still too young to know that life never gives anything for nothing, and that a price is always exacted for what fate bestows.
那时候她还太年轻,不知道所有命运馈赠的礼物,早已在暗中标好了价格。
——茨威格
总结
第三次作业不仅没有想象中可怕,还是三次作业中耗时最少的一次。得益于层次化结构的设计,新增求导运算的升级过程非常顺利,唯一的注意点是数学上的求导法则,需要在编码时严格遵守。只是可惜我的侥幸心理导致互测中刀,现在想来仍然感到深深可惜!!!
测试
本单元由于身体状况与个人时间的关系,只在第一次作业搭建了评测机,后续使用了cxc同学的评测机,这里再次感谢cxc同学!!!
在个人搭建评测机的过程中,我主要还是使用模拟递归下降的过程构建测试数据,并利用 Python
中的 Sympy
包来达到化简表达式并判断正误的效果,唯一的缺点是速度太慢。同时,我在构建评测数据时特意加入了诸如 x-x
,+00000
等数据,来保证随机生成数据的特殊性,但是只是可惜评测机hack效果不佳,更多还是靠手动构建数据。
心得体会
第一单元就这样痛并快乐地坚持了过去,但是和同学们投入的时间相比,我好像完成得还算轻松(?)
通过表达式解析的练习,我深刻体会了面向对象的设计思想,了解了递归下降的解析方法,初步尝试了层次化的类设计,这也为OS的Lab1提供了一些帮助。
最后还是感慨:设计为先,一个好的设计能为后续的编码工作省去不少麻烦;就比如,每次作业前,我都要花上一天半来进行设计,而不是基于上手编码,否则代码必然是漏洞百出。此外,还必须注重可扩展性,即在当前设计中打一个提前量,给未来可能引入的设计提前预留一些空间。
打个比方,我的程序在第一次就支持括号嵌套,第二次就支持自定义函数嵌套,现在第三次作业的程序还支持多变量运算与求导等等。
未来方向
总体而言,我认为课程组对于第一单元的设置已相对完备,但是第三次作业相较前两次明显过于轻松,我认为可以加入多变量运算与求导等要求。同样,我认为强测应该设置专门数据来卡时间复杂度,这样就能狠狠整治向我这样心存侥幸的同学!!!