OO第一单元小结
总体架构
- 表达式建模
根据对于表达式的文法,对待化简的表达式建模。
表达式由项组成,项由因子组成,因子分为变量,常数,指数函数,表达式因子,求导因子,自定义函数因子。
-
展开后的表达式
最后的表达式展开为一个每一项都为 A*x*exp^() 的标准形式。
总体架构还是比较清晰的,根据表达式的具体组成划分了相应的类,架构的层次化和问题的层次化基本一致。因子类根据其行为的共性toString()和toStdExpr()提炼出接口Factor,所有因子都实现了接口,实现了各因子引用和函数调用的统一。在存储函数定义的部分单独设置了一个类,并使用了单例模式。
不足之处在于Expr类同时包含有未化简的项和化简的项两个属性,可以单独再设置一个Poly类来存储已经化简了的项,这样可以更进一步地对Expr类中的职能进行划分,使得逻辑更加清晰。
架构,思路分析
a. 表达式解析
-
递归下降
递归下降的具体实现在公众号中已经讲解的很清楚了,这里只记录一些对于递归下降方法的总结和思考。
-
递归下降这种方法应用的前提是一个问题具有很清晰的层次结构,而这也是第一单元的训练目的。
-
其次就是该问题是具有递归形式的,典型的体现就是表达式因子的解析又回到了表达式这一层。但与此同时,它又不是无限递归的,即它有递归的终点,就是类似常数,变量这种因子。
对于这种问题的处理不能采用一般的“一杆子杵到底”,因为递归的深度是无法预料的,没有办法一把解决问题。通过层次结构,每一层解析对应层所包含的内容,然后交给下一层去解析下一层的内容,这种层次结构,不仅将代码逻辑变得清晰,更重要的是它保持了问题的本身的结构。这也为递归调用提供了保障。
在较高层的代码中直接调用下层的代码,假设接下来的问题已经被下层代码所解决,直接返回解析结果。就是这种层次化的调用实现了一次下降,而不断调用也就形成了递归。
我认为,这种在高层次代码中假设低层问题已经被解决并调用低层方法得到结果的思路正是递归下降的核心。
同时,这种递归下降的方式也是对问题非常好的层次化建模。
-
b. 表达式化简
-
将一个表达式展开,需要将表达式化成一个标准多项式,实际上就是将其中的每一项进行统一表示,从而可以进一步合并同类项。
-
项又是由因子构成的,根据递归思想,需要将所有因子统一表示,同样统一表示为标准多项式,标准多项式由标准项构成
仔细观察所有因子发现这些因子可以分成两类,底层因子和非底层因子。
-
所谓底层因子就是不能继续化简的原子因子,有常数,变量,指数函数 [注]
-
非底层因子就是由底层因子组成的可化简的因子,有表达式因子,自定义函数,求导因子
对底层因子来说,只需要自己的类型创建标准项实例,再创建标准表达式即可
对于非底层因子,如表达式因子,实际上就是由一个表达式在加上一个指数组成。所以就先将表达式化成标准多项式,然后再调用标准多项式相乘的方法。总结来说,本质上就是通过递归调用toStdExpr()方法,然后再根据因子自己的计算规则,进行标准表达式的运算即可。在递归调用的过程中,如果调用者是底层因子也就意味着递归到底了,就按照底层因子的toStdExpr()返回标准多项式。
[注]: 指数函数中的因子可以继续化简,这里指化简因子后的exp()整体
-
事实上,对于表达式化简的过程,也可以归结为是一个递归下降的过程,具体过程和表达式解析过程很类似,只是具体处理不同,但结构都是一个递归下降的结构。
度量分析
这是所有类的复杂度
这是复杂度最高的一部分方法的复杂度
根据复杂度分析来看,最复杂的方法其实是输出标准项。复杂的原因是里面设计到了对于各种特殊情况的判断,如系数为1,-1,有没有exp指数等等导致复杂度剧增。然后就是处理token时一系列的if-else判断。
事实上,在逻辑上最为核心的化简表达式的过程的复杂度并没有太高,究其原因是合理的层次划分和统一的处理判断。这再一次体现出了架构设计的重要性。对于递归下降解析表达式的过程,解析表达式和解析项的方法的复杂度都不高,而解析因子的复杂度较高是因为这里用了if-else做了因子类型的判断,调用了其他多个解析因子的函数。
Bug分析
a. 自己的bug
每次作业的Bug都只有一个点,并且不是代码逻辑上的错误,即整体问题的建模框架是没有问题的,而是一些具体处理的细节。
- parser中解析指数时,在某一个分支中解析后忘记移动指向当前token的指针,导致解析指针的位置往后错了一个。
- 在函数参数替换中,求导因子替换没有整体加括号,导致运算的顺序发生变化。
正如Bug这个词一样,它确实就是很细节的一个地方的错误。对于这类bug出现的原因,其实并不是它的逻辑上有多么复杂,就是因为它太过于细小。
而我认为减少此类bug的一个方法就是归一化处理,尽可能的屏蔽细节而提炼共性,是减少此类bug的一个很重要的方法,而归一化处理则又依赖一个相对合理的架构,所以问题又回到了对于架构的设计上。当然,一些细节是避免不了的,所以另外能做的就是尽可能充分的测试来发现这些bug。
b. 别人的bug
我测试其他人的代码思路先是用随机生成的表达式进行测试,如果没有问题的话我会去看一看别人的代码,并针对其中的逻辑手动编写一些数据。
对于bug来说,数据不见得要有多么复杂,而是得正好包含那个很细小的点。所以我一般是考虑题目中的一些特殊情况,例如多符号,嵌套,再结合代码的逻辑中比较具体的某些点进行测试。如对于函数参数的替换顺序问题使用最简单的f(y,x)= x-y就能测试出。
另外,我debug也是使用这个思路,对于一个输入来说。我是先猜测其中的bug可能出在哪里,并将很小的一块先单独提出来,然后再测试看这个小块的结果是否正确。这种方法在我的debug过程中非常有效,通常可以很快就锁定问题出现的某个点上。我绝大部分bug都是采用这种方式来找的。
优化
优化的基本原则应该是保持原有整体结构的清晰和功能的正确性。
优化的过程主要在合并同类项上,而在合并同类项的过程中我采用了先排序,再合并的思路。
因为我没有采用hashMap的结构来存储,所以不能利用key的唯一性做相加,同时为了避免多层循环嵌套遍历的情形,我采用了先按照指数排序,然后只需顺序遍历,合并相邻的具有相同指数的项即可。
其他的一些优化包括最终对系数进行排序,让正系数在前输出。0,-1,+1系数的输出等。
一个迭代
自定义函数中包含求导因子。再添加其他类型的因子如对数函数等。
对于自定义函数中包含求导因子,我的原有代码并不需要进行改动。我对于自定义函数因子的处理方法是将实参带入形参中得到带入后的字符串级别的表达式,然后再对表达式进行解析,化简。所以这仅是在一个表达式中添加了求导因子,而原来的架构就能解析求导因子。
对于其他类型的因子如对数函数新建一个因子类,让它实现factor接口,并且在标准项中额外添加对数函数的属性,同时修改标准多项式的乘法。总的来说,原有架构不用改变,只需要做扩展性的因子补充,和一些方法的扩展即可。
心得
首先,通过一个单元的学习,我认为这门课本身就是一门非常注重实践类的课程。理论上的知识大都是比较抽象,原则类的东西。只有通过实践,才能真正体会到这些理论如何在实践中进行运用。而且很多东西也是要在实践中去一步步领悟的,就拿架构设计来说,只有通过具体的问题的思考解决才能领会层次化设计在具体实现中的应用,这也是个不断积累经验的过程。
在整个迭代过程中,从0到1的突破是最难的。第一次作业对我而言是最困难的,规划架构,还要考虑功能的具体实现,前几天的苦苦思考,最后一天的debug,再到临近截止时间的完成,整个过程确实不易,但完成后的快乐让我感觉我又能多活十年。后面的迭代在整体架构上没有大的调整,所以相对第一次更为顺利。
最难的。第一次作业对我而言是最困难的,规划架构,还要考虑功能的具体实现,前几天的苦苦思考,最后一天的debug,再到临近截止时间的完成,整个过程确实不易,但完成后的快乐让我感觉我又能多活十年。后面的迭代在整体架构上没有大的调整,所以相对第一次更为顺利。
对于未来的方向,我觉得在第一次作业可以做一些拆分,简化一部分要求,然后再后续作业实现。或给予更多的设计引导,这样对同学们来说可以更好的完成从0到1的突破。