一、程序结构和架构设计体验
HW1
类图:
各类复杂度:
各类方法复杂度:
代码规模:
hw1 的任务是展开包含+ - * ^ 的变量为x的表达式,最多有一层括号。
在train1的启发下,我采用了递归下降法来解析表达式。
以下是在构建框架时写的思维导图:
在Parser类中,我构建了ParseExpr ParseTerm ParseFactor 等方法来进行表达式解析。
在计算化简这一步,我采用了一个思维难度较为简单的方法,即将所有的 Expr , Term , Factor 统一用"Factor"类来进行储存,Factor类中构建一个Hashmap<key,value> key值为指数,value为对应的系数。
分析一下可以发现,无论是Expr,Term还是Factor,它最终化简的结果都可以用如下结构来进行表示:
∑ \sum ∑(value ∗ \ast ∗ x key)
在化简过程中可能会出现Expr * Term 或者Expr * Factor等复杂情况,用这种方法,只需要考虑不同"Factor"之间的Hashmap的合并,不需要考虑它们具体是什么。
这样做的优点是代码较为简洁,在后续的学习中我们知道像类似的情况可以用继承来实现,从而降低代码的复杂度,提高扩展性。
这样做的缺点是Factor类的复杂度过高,且扩展性较差。
HW2
类图:
各类复杂度:
各类方法复杂度:
代码规模:
hw2在hw1的基础上增加了多层括号嵌套和自定义函数调用以及指数函数exp
多层括号嵌套的问题可以用递归下降法解析表达式进行解决。
对于自定义函数调用,我首先新建了一个Function类,用来存储函数的参数以及表达式,并在Parse类中新建ParseFunction方法对其进行解析。这样Function类包含了自定义函数的所需信息。
然后再对表达式进行函数带入。这一部分我先递归到最里层自定义函数,然后用replace函数进行替换,再回溯到外层函数依次进行替换。简单来说就是从里到外进行替换。为了避免exp中的x也被替换等情况,我们在ParseFunction时需要将式子中的x y z替换成_x _y _z , 参数中的x y z 也替换为 _x _y _z。
为了解决新增指数函数这一问题,我对存储方式进行了重构。
分析可知:
Expr = ∑ \sum ∑ A ∗ \ast ∗ x b ∗ \ast ∗ exp(Expr1)
我构建了一个Term类,它存储了上式中的 A 、b 、以及 Expr1
还构建了一个Expr类,存储结构为 ArrayList < Term >
Term类和Expr类在计算过程中mulExpr , addExpr , mulTerm , addTerm等函数相互进行递归调用,写的时候只需要考虑每个函数怎么写,将复杂的计算过程化整为零变成较为简单的子问题。
在写计算合并的过程中我遇到了一个较为棘手的问题,即深浅拷贝,显然这里的Expr需要进行深拷贝,在写的时候需要格外注意。
合并判断两个类是否相等时,需要自己重新写equals函数。
HW3
类图:
各类复杂度:
各类方法复杂度:
代码规模:
hw3在hw2的基础上增加了求导算子dx,允许自定义函数调用其他自定义函数。
对于允许自定义函数调用其他函数这一问题,我们可以在预处理函数阶段进行解决,即修改ParseFunction。在ParseFunction时遇到其他函数例如
h(x) = f(x + g(x))
现将g(x)替换为表达式,在将f替换为表达式,以此类推。
对于求导因子dx,可以将dx()中的内容看做表达式,先将表达式化简然后对其进行求导。
由hw2我们知道
Expr = ∑ \sum ∑ A ∗ \ast ∗ x b ∗ \ast ∗ exp(Expr1)
因此
dx(Expr) = ∑ \sum ∑(A ∗ \ast ∗ b) ∗ \ast ∗ x b-1 ∗ \ast ∗ exp(Expr1) ∗ \ast ∗ dx(Expr1)
只需要在Term和Expr类中新增deduce求导函数即可。dx(Expr) 递归到最内层时,exp中为空。
自定义新迭代场景
新增sin,cos等三角函数。
在Term中新增Expr1 Expr2,用来存储sin(Expr1)中的Expr1和cos(Expr2)中的Expr2。
Expr = ∑ \sum ∑A ∗ \ast ∗ b x ∗ \ast ∗ sin(Expr1) ∗ \ast ∗ cos(Expr2) ∗ \ast ∗ exp(Expr0)
合并时改写计算方法即可。
bug
在OO Unit1学习中我在第一次互测出现了bug。原因是没考虑清楚0 ^ 0 和 0 的冲突问题,在hw1中我将常数存储为形如 a * x ^ 0的形式,这就会导致0 ^ 0 和 0 出现冲突,解决方法为遇到0则不将其存入hashmap中,计算过程中遇到0也同理。我采用了此方法但是没有在第一次作业中对所有函数进行充分的修改,导致出现bug。hw2 和 hw3 未出现bug。
需要通过构建评测机,对拍等方式对代码进行充分的测试,保证每个部分的正确性。
在互测中我们可以构建嵌套层数较多的函数进行测试,部分实现方式可能会因为没有及时化简导致复杂度出现问题从而TLE。
优化
我在输出部分进行了优化,省略了没有必要的+ - 和 1 以及括号。例如 -1 ∗ \ast ∗ x可以化简为 -x , 1 ∗ \ast ∗ x 可以化简为 x , exp((x)) 可化简为 exp(x)。
边计算边合并,避免式子过长导致复杂度较高。
我在Expr类中建立了print方法用来输出,与计算化简过程分开是一个单独的模块,可以保证代码的简洁性和正确性。
心得体会
OO的学习需要建立起面向对象编程的思想,继承和接口是构建大规模代码的重要工具。我们可以首先对问题进行分析,然后将其拆分为一个个子问题进行解决,写的时候只用考虑这一个子函数怎么写,调用时则不需要考虑它具体是怎么实现的,实现高耦合低内聚。我习惯将问题简单化,这使得代码行数较短,但是可扩展性也相应地较为欠缺。之后的学习中可以更多地使用继承和接口,使得代码更加层次化,面对更加复杂问题能够得心应手。
需要对自己代码的各个函数进行充分的测试,保证每个子函数的功能符合预期,这样能够保证代码整体的正确性。debug是写OO必须面对的事情,找到问题所在的板块再针对性的解决它。
未来方向
OO第一单元的课程整体来说难度较为适中,对于上过OOpre有一定java基础的同学学习难度较为适宜,但hw1到hw2之间的难度提升较大,而hw2到hw3的难度又较小,可以将hw2的难度适中一下,使得难度跨度差别小一些,更有循序渐进的效果。