BUAA OO 第一单元博客总结

BUAA OO 第一单元博客总结

一、综述

《面向对象设计与构造》的第一单元迭代作业的主要内容就是对表达式进行化简处理,其中包括基本的四则运算、多层括号嵌套、自定义函数、指数函数(exp)以及求导的运算法则。这些任务分为三次作业,要求我们在每次作业上进行迭代,实现新增的功能。而在本单元的作业中,贯穿其中的一个重要想法就是递归下降的思维。递归下降的思维很大程度上帮助我们处理了许多复杂的表达式,为我们化简计算表达式提供了可能。

二、基于度量分析程序结构

1.UML

下图是本单元作业代码的UML图:
在这里插入图片描述

首先是Factor为一个接口,下面有Expr、ExpSub、Term、Pow等诸多类,在parse()里面用来解析表达式。
其次是preprocess()是用来预处理表达式的,主要是处理一些多余的符号以及自定义函数。
然后是Mono和Ansout(Poly),分别用来存储单项式和表达式,以及完成最后的合并和输出功能。
最后是Cal类,其功能是用来计算表达式的各种运算。

2.代码规模

下图是三次作业迭代后的代码规模,一共有1087行,源码规模990行
在这里插入图片描述

其中代码行数最多的是preprocess()和Ansout()。其中preprocess()中对自定义函数的处理占用了较多的行数,实现起来需要较多行数。其次是Ansout(),用来存储多项式以及多项式的合并和输出。

3.类复杂度

在这里插入图片描述

可以看到复杂度较高的就是Ansout()和Cal(),因为其中涉及到计算和合并,所以需要用到递归的思想,使得复杂度大大的提高了。
下图是各个类中方法的复杂度
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

其中,复杂度高的几个方法就是上述提到的运用了递归的思想导致的。

三、整体代码结构分析

1. 基本思路

本次作业的基本思路就是,将得到的表达式先进行预处理,去除不必要的空白符、相连的加减号等。然后通过递归下降的方式对表达式进行处理,得到其后缀表达式。最后再通过对后缀表达式的计算并合并,得到最终的化简结果。

2. 核心思路

在本单元的作业中,预处理和递归下降处理表达式都属于较为简单的部分,需要思考的就是如何对含有变量x的单项式进行四则运算等计算,并最终将同类项进行合并。所以在这次作业中,我们设计了一个基本项的思路,即对于每一个单项式,我们都可以通过统一形式的基本项表达出来,这样就可以以基本项的形式进行计算合并的操作。

3. 三次作业迭代

3.1 第一次作业
3.1.1 作业要求

读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号后的表达式。

3.1.2 架构分析
3.1.2.1 表达式预处理

首先是我们输入的表达式可能含有空白字符、多个符号相连(至多三个)、数字前面存在前导0等,为了后续在parse()里面能够方便处理,提前在perprocess()里面对表达式进行处理。

其次是对减法的特殊处理。在最初思考的时候,遇到了一个问题:
x ∗ − 4 ∗ − x x*-4*-x x4x
这种情况下的负号如何处理。经过思考后,我对减法运算的处理就是把它也当成加法,相当于加上一个负数(正数和负数可在存储基本项的时候用一个flag进行判断)。所以我需要特殊处理一下负号,主要包括对于负号前面是左括号或者是乘号或者是表达式最开头的地方。处理的方法就是在前加上0,把它看成一个加法。所以上述的表达式就会被处理成下述的表达式。这样在parse()里面处理的时候就比较简单。
x ∗ ( 0 − 4 ) ∗ ( 0 − x ) x*(0-4)*(0-x) x(04)(0x)

3.1.2.2 递归下降解析表达式

主要是通过lexer()和parse()进行解析表达式。

lexer()的作用主要是通过next()来逐个读取表达式的下一个数字或者字符,peek()返回目前读取的值。
parse()里面则是利用递归下降的思想解析表达式。
在第一次作业中可能遇到的运算符有"+“、”-“、”*“、”^“。我对其的运算优先级进行判断,在最初的Expr、Term和Factor的基础上增加了ExSub(用于解析”-“)和Pow(用于解析”^")。在最后的Factor类中,可能出现的因子有:变量因子、常数因子以及表达式因子。
如果是变量因子和常数因子,识别到之后,则返回自己属于的类。
在这里插入图片描述

如果是表达式因子,则将其内容当成表达式,再次调用parse()进行处理。处理结束之后,再通过lexer().next(),把右括号读过。具体如下图
在这里插入图片描述
最后,再解析完表达式之后,就可以得到表达式的后缀形式,接下来就是对其后缀表达式进行计算。

3.1.2.3 遍历计算

首先是,前面提到的核心思想:构造出一个基本项来存储每一个单项式并计算。所以我们创建一个Mono类,其用来存储每一个项。其基本的属性包括coefficien(系数),index(变量x的指数)以及flag(判断正负)。
基本项的形式就是 a x b ax^b axb
接下来我们就对得到的后缀表达式expr进行遍历。如果遇到常数和变量x,则新建一个Mono类的对象,用来存储其相关内容。
其次,我们在计算的时候,会存在有多项式的情况,即我们会对单项式进行加减乘或者乘方运算,得到多项式。而多项式又是由至少一个单项式组成,所以我们新建一个AnsOut类(其实就是Poly类)来表达多项式,以及用来计算输出最后的结果,其属性就是一个以Mono对象为存储的容器。
所以在遍历后缀表达式的时候,我们会通过一个
在这里插入图片描述
来存储表达式。当遇到运算符号的时候,从我们容器中取出最上面两个的多项式进行计算(栈的思想)。
当遇到负号的时候,分清楚减数和被减数,并对减数的flag进行翻转。
当遇到乘号的时候,调用CalMul()进行计算。在计算的时候,我们就通过单项式的基本项进行计算。计算后将新的多项式存入上述ansouts里面。
在这里插入图片描述

当遇到乘方的时候,则取出其指数,然后调用CalPow()进行计算。
在这里插入图片描述
而乘方本质上就是连乘,所以在CalPow()中,我们就通过循环来多次调用CalMul进行乘方的计算,最终得到计算结果。
在这里插入图片描述

通过上述计算,我们最终就可以得到一个多项式,这个多项式里面存储着一个又一个的单项式,然后通过flag来判断单项式的正负,即加上该单项式还是减去该单项式。
∑ a x b \sum{ax^b} axb

3.1.2.4 合并单项式并输出结果

在我上述计算结束之后,得到一个多项式,多项式有多个单项式相加减得到,但是还未合并同类项,所以在最终的输出前,我会通过Merge()函数合并同类项。因为单项式的存储方式都是以一个统一的基本项,所以我们判断是否能合并的条件就是变量x的指数是否相等。相等即可合并。
在这里插入图片描述

在合并之后,即可遍历多项式中的每一项单项式,然后将其输出。在输出的时候,我们对于一些特殊的情况可以进行化简:系数为1、指数为1等这些可以不输出。

3.2 第二次作业
3.2.1 作业要求

读入一系列自定义函数的定义以及一个包含幂函数、指数函数、自定义函数调用的表达式,输出恒等变形展开所有括号后的表达式。

3.2.2 迭代架构

本次作业新增了三个要求:多层括号嵌套、自定义函数调用以及指数函数、

3.2.2.1 多层括号嵌套

多层括号嵌套的需求在之前的代码中已经实现:遇到左括号即把其当成表达式因子,然后递归调用parseExpr();如果在调用过程中继续遇到左括号,那就继续递归下去,直到退出。

3.2.2.2 自定义函数调用

对于自定义函数的分析和处理,有两种选择:一是在最开始预处理的时候,就将自定义函数代入,即字符串替换。二是在parse()函数里面进行解析表达式的时候,遇到自定义函数将其当成一个因子进行分析处理。第一种的思路比较直接明了,所以我在第二次作业迭代中,在preprocess()的函数中,直接将自定义函数代入处理,得到一个不含自定义函数的表达式,然后再进行下一步的处理。
对于直接代入自定义函数,主要分为三步:把自定义函数的形参和函数体存储起来,把调用自定义函数的参数也存储起来,最后进行字符串替换。
首先是第一步:把自定义函数的形参和函数体存储起来
在这里插入图片描述
其中容器formal用来存储xyz,str代表自定义函数的函数体。
第二步:把调用自定义函数的参数也存储起来
在这里插入图片描述

当遇到自定义函数的函数名fgh的时候,我们就开始存储调用自定义函数的参数,将其存储在formal2的容器中。然后通过括号的个数和逗号来判断参数的个数。
当如果遇到调用自定义函数的时候,其参数继续调用自定义函数,我们也递归调用上面函数,进入下一层,直到没有自定义函数的调用,则进入下一步的字符串替换。
最后一步:进行字符串替换
在这里插入图片描述
在这里插入图片描述
在submissionFunction()中进行一对一替换。遍历formal1的容器,如果存在xyz,则对自定义函数的函数体进行对应的替换,替换的字符串,从formal2容器中取出。最终得到替换后的字符串,并返回。
需要注意的是,因为本次作业中新增的指数函数的表达形式是exp(),其中也含有x,所以在对x进行替换的时候,我们需要提前将exp替换成其他字符,防止在替换x的时候出现问题。

3.2.2.3 指数函数

在本次作业中新增了一个新的因子:指数函数,其表达形式为exp()。
在预处理的时候我们不需要对exp()进行处理。
解析表达式的时候,我们在解析因子的时候又多了一种可能:指数函数。
所以我们新建一个类Exponential()用来解析指数函数。在parse()类中的parseFactor()方法中,我们进行多一种判断,判断其是否是指数函数。
在这里插入图片描述
如果是,将exp()括号中的内容当成一个表达式进行递归分析,然后返回Exponential类的对象。
在接下来的计算合并中,我们主要的依据手段就是基本项。现在新增了指数函数,我们的基本项就需要发生变化。需要从原来的形式中增加exp(),具体形式如下:
a x b e x p ( E x p r ) ax^bexp(Expr) axbexp(Expr)
其中,exp()里面的内容可以是一个表达式进行嵌套,所以我们在存储的时候就需要考虑到这一点。
所以Mono类的属性就需要新增一个来存储exp()。
在这里插入图片描述
所以在存储exp的时候,我们就需要将栈最顶层的多项式作为其指数内容,具体如下:
在这里插入图片描述

接着就是对表达式的合并,其主要操作和第一次作业类似,唯一不同的就是如何判断基本项是否相同。其中判断基本项是否相同比较重要的就是判断 e x p ( E x p r ) exp(Expr) exp(Expr)里面的Expr是否相同,且存在exp嵌套exp的可能。
所以我增加了一个新的方法eIndexEqual(),专门用来判断基本项中的exp上的指数是否相同,其主要方法就是遍历两个指数,判断其是否完全相同。如果相同则返回true,反之返回false。
在这里插入图片描述

在合并之后即可按照原来步骤进行表达式输出。

3.3 第三次作业
3.3.1 作业要求

读入一系列自定义函数的定义以及一个包含幂函数、指数函数、自定义函数调用、求导算子的表达式,输出恒等变形展开所有括号后的表达式。

3.3.2 迭代架构

本次作业新增两个要求:在定义自定义函数的时候,可以调用已经定义过的自定义函数如:
f ( x ) = x + 1 , g ( y ) = f ( y ) + y 2 f(x)= x+1, g(y)=f(y)+y^2 f(x)=x+1,g(y)=f(y)+y2
另一个要求是实现求导算子dx()。

3.3.2.1 自定义函数

这个定义时存在嵌套调用其实很容易处理,由于最多只可能有三个自定义函数,所以嵌套调用至多只有两层,所以我们只需要调用两次preprocess()对表示式进行处理,即可得到不含有自定义函数的表达式。然后将其传入parse()里面进行解析即可。

3.3.2.2 求导算子dx

求导算子可以当成一种新的运算法则,只需要在处理的时候运用好正确的求导法则即可,相对而言比较好实现。
类似的,把求导算子当成一个因子处理。所以新建一个Derivation类,用来专门返回求导算子因子。
在这里插入图片描述
然后在计算的时候则递归调用求导的方法Der()进行求导即可。
最后合并输出的过程与第二次作业相同,无需过多的修改。

4.新迭代情景

假设在第三次作业的基础上继续迭代,增加三角函数sin()和cos()的运算。首先是识别三角函数,这一步相对来说比较好实现,只需要在parse()中新增两个因子类即可。最主要的是思考在新增之后,基本项的形式如何设计。
a x b e x p ( E x p r ) s i n ( E x p r ) c o s ( E x p r ) ax^bexp(Expr)sin(Expr)cos(Expr) axbexp(Expr)sin(Expr)cos(Expr)
是否可以将任何单项式都转换成如此的基本项,其中需要运用相关的数学公式。其次是,在变换的过程中,系数a已经无法保证一定为整数,需要对其进行改动。所以整体的架构对于一些新的要求来说,拓展性还有待提高。

四、强侧与互测

bug

在第一次作业中,我由于对负号独特的处理(将"-1"变成"(0-1)")的过程中没有思考全面,忽略了一种情况,导致在强侧和互测中被hack。
在第二次作业中,首先是强侧的两个点运行时间过长,爆了内存。在后面的debug中,发现是在乘法的运算中,循环太过复杂导致的。所以,我在每一次乘法结束之后,都会提前将这个表达式进行一次合并,减少循环的次数,很大程度上提高了运算的速率和降低了内存的使用空间。其次是互测由于括号的规范性问题而被hack。在后续的debug中,也是发现在处理输出括号层数不符合指导书的要求而被hack了。
在思考后,我认为是我自己对题目的理解有所欠缺,没有思考全可能出现的各种情况。其次,是我没有完全的面向对象编程导致的。对于出现的问题,我只是简单地从表面去思考,企图通过用一大串代码去解决这些问题。而大篇幅的代码就非常容易出现潜在的bug。最后是,在思考代码架构的时候,并未将复杂度加入思考的范畴,从而导致很多高复杂度的计算过程,使得代码的性能较低。
所以在之后的作业中,我需要花更多的时间去理解题目,并设计更好、更具有拓展性的架构。同时,要避免大篇幅的代码,而是要实现高聚合,低耦合。

hack

在互测环节,首先是利用现有的测评机对其他人的代码进行评测。如果出现问题,则对出现问题的样例进行分析,分析其出现bug的点是什么,然后提交进行hack。如果没有发现相应错误,一方面可能是:代码本身没有错误;另一方面是测评机功能有限。这个时候,则会根据代码中较为可能出错的地方进行针对性测试数据进行hack。

五、优化

第一次作业优化

第一次作业的优化较为简单,主要是将不必要的0去除。如果一个0的前后存在加减号,则可以去除。如果只剩下一个单独的0则不去除。其次是将正数先输出,这样可以减少一个字符。实现方法也是遍历,如果第一项是负号,且后面存在正项的单项式,则将后面正项单项式先输出。

第二三次作业优化

首先是将exp(0)优化成1。具体实现过程是在存储计算的过程中,如果出现exp(0),则将其直接存储为1。这样在后续的计算中则不会出现exp(0)。
其次是对exp(Expr)中的指数提取公因数。这个主要的实现方法就是遍历Expr的容器中每一项,计算出其系数的最大公因数,然后将其提出优化。但是这个操作在我的代码架构上难以实现,在实现的过程中,对于之前以后的代码会产生较大的变动,使得代码的简洁性和正确性大大降低。所以,在最终的代码中,我没有实现该优化。究其原因,是在最初设计代码架构的时候考虑的太少,没有思考到后续的优化步骤而导致的。所以在之后的单元中,不仅仅是要具有可拓展性来完成新增功能,同时也要有优化的空间。

六、心得体会

  1. 首先是第一次接触到递归下降的处理思路,深刻体会到其处理表达式的便利之处。它可以让我们更方便地处理复杂的表达式,不需要我们去逐个分析表达式的每一个字符,而是将表达式划分成Expr-Term-Factor这样一个具有严格逻辑的层次,然后我们在这样的一个层次上一步一步深入去解析复杂的表达式。这样处理的好处就是我们只需要专注于每一个层次可能出现的各种情况,比如可能出现的因子有哪几种,将其分类清楚即可,而不需要我们去具体处理整个表达式。
    递归下降的思想,让我们在处理表达式的时候,合理地屏蔽细节,将具体的实现封装,让整体的代码更加高效且清晰。
  2. 其次是进一步认识到一个合理的、具有可拓展性的代码架构的重要性。这很大程度上决定了在之后迭代作业的过程中,实现新增功能的难易程度。所以在最初设计代码架构的时候,我们就应该思考到将来可能出现的迭代任务要求,基于这些可能出现的要求去思考设计我们的代码架构。这样在之后的迭代作业中,我们就可以基于最初的代码架构很好地实现新增的功能,避免了不必要的重构或者重写。当然,当发现自己的架构很难完成新增功能,我们就要即使进行重构,以此来完成新增功能,并增加代码的可拓展性,以便完成之后的迭代任务。
  3. 最后是搭建测评机的重要性。搭建出合理的高效的测评机,一方面,可以帮助我们发现自己代码可能出现的问题,降低强测出错的概率。另一方面则是在互测的时候,可以更加高效地寻找他人潜在的bug。

七、未来方向

在取得同学的同意前提下,可以公布代码架构良好、具有高拓展性、高性能的代码,以此供大家学习参考。

  • 20
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值