BUAA OO Unit1总结

BUAA OO Unit1总结

前言

第一单元学习的主题是表达式的展开,体会面向对象层次化设计的思想。三次作业逐步迭代,从最初的仅包含基础运算符到包含指数函数、自定义函数和导函数,尽管在过程中被架构设计,诸多bug和更加全面的数据构造所困扰,甚至废寝忘食,但回过头来,三周的训练仍然让我受益良多,尤其是在层次化架构上进步显著。

由于从第一次作业开始就选择了较为适宜的递归下降的算法,因此在作业过程中并没有进行大规模的重构。第二次作业对于自定义函数的处理是递归的字符串替换,这也满足了第三次的在函数中调用函数的要求。第三次作业的求导部分则是借鉴了实验中的设计,将整个求导过程也梯度下降化,分别在表达式、项、因子中实现求导方法。下面是具体分析。

第一次作业分析

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

首先通过类图来分析整体结构

请添加图片描述

具体分析如下

代码整体可以分为两个部分:

  1. 除去DanXiangDuoxiang两个类之外的架构主要负责将表达式、项和因子解析出来,这里采用了梯度下降算法:
  • Lexer(词法分析器)在我的设计中作用较简单,就是读取字符串中的下一个部分,并通过 peek() 函数获取,其中在读到数字时会通过 getNumber() 读取整体数字。
  • Parser(解析器)是解析部分的核心,会自上而下依次递归生成表达式、项、因子,为了解析 peek() 传入的基本单位,在架构过程中加入了许多对传入字符的判断,这也导致了这一部分的复杂度较高,这是后续优化的一个重要思路。
  1. DanXiangDuoxiang两个类主要执行展开表达式的任务,这主要是考虑到最终的输出形式总是多项式,可以拆分成形如 a*x^b 的若干个单项式的和的形式。
  • 在具体实现上,DanXiang类包含系数和指数以及表示正负的sign变量,Duoxiang类含有一个HashSet容器,用来容纳单项式。
  • ExprTermFactor各类中均实现toDuoxiang() 方法,将类中的内容转化为多项式形式,从factor开始向上转化,最终通过 expr.toDuoxiang() 获取最终结果。
  • 更具体地,为了实现 toDuoxiang(),在Duoxiang类中包含了add()mul()pow() 三个方法,其中后两个方法分别解决由因子相乘组成的项的转化和含有指数的表达式因子的转化。

其他方面

  1. 需要对字符进行预处理,设计中采用了replaceAll() 的正则表达式替换方法,去除了输入字符串中的空格、换表符,并提前处理了出现多个正负号相连的情况
  2. 关于优化:为了输出最终结果,分别在DanXiangDuoxiang两个类中重写了toString() 方法,同时在输出时需要实现:
  • 单项式系数为1时省略。
  • 单项式系数为0时整体为0。
  • 单项式系数为-1时转化为负号。
  • 指数为0时只输出系数。
  • 指数为1时可以省略指数。
  • 输出时如果多项式包含的若干个单项式中含有系数大于0的项,将其置首。

代码复杂度分析

请添加图片描述
请添加图片描述
大部分方法的复杂度在合理范围内,主要有三个方法超标:

  • parser类中的parserFactor() ,主要是由于因子本身可以分为三类,在解析过程中经过多次判断来确定具体是哪种因子,导致复杂度较高。
  • DanXiangDuoxiang类的toString() 方法,这里的原因详见上一节末尾,主要是对于输出结果的优化需要判断系数、指数具体是什么,进而选择不同的输出策略,也导致了较高的复杂度。

遇到的bug及第一次作业整体感悟

  1. bug的发现与修改
  • 第一次作业的强测和互测过程较为顺利,遇到的主要bug是在构造数据测试过程中发现当输入表达式含有带指数的表达式因子时会出错。
  • 经过分析后发现问题出在Duoxiang类中的pow() 方法,在实现过程中具体是将幂函数转化为多次调用mul() 方法与自身相乘,但在实现过程中,我混淆了深、浅拷贝的使用,导致在乘法运算过程中改变了乘数,因此结果会呈现指数式增长。
  • 修改bug时改变了克隆方法,采用创建一个新的Duoxiang,并依次加入预拷贝对象包含的单项式的方法。
  1. 整体感悟
  • 因为很长时间没有接触过java代码,在写第一次作业时还是很生疏的,甚至不得不通过百度查阅部分语法。因此,经过第一次作业首先是唤醒了身体中沉睡的java之魂。
  • 此外,第一次作业显著提升了我层次化架构的能力,对于梯度下降算法的多次应用也让我开始习惯了自上而下,隐藏下层细节解决问题的思路,为之后作业的迭代建立了良好基础。
  • 另外,在这次作业我也开始改变之前一边写代码一边架构的坏习惯,开始提前构思好整体设计再开始敲击代码。

第二、三次作业分析

由于在第三次作业中对代码架构几乎没有任何修改,仅仅是在ExprTermFactor 类中新增求导方法,这里将两次作业的架构一同分析。

作业要求

  • 第二次作业的具体要求是新增多层括号,指数函数和自定义函数;
  • 第三次作业的要求是新增在函数中调用函数和求导因子;

由于整体设计始终应用递归算法,因此新增多层括号和函数调用函数都在之前的设计中兼容,所以在具体实现过程实际上仅仅新增指数函数、自定义函数、导函数三种因子。

首先通过类图来分析整体结构(以第三次作业为例)

请添加图片描述

具体分析如下

可以看到代码的整体结构与第一次作业类似,仅仅新增了三个类为新增的因子服务,下面分别分析对于新增的三种因子的处理。

指数函数

在引入指数函数后,多项式的组成单位发生变化,即单项式由形如a*x ^ b 转化为形如a*x^b * exp( c ) ^d ,具体构造方法如下:

  1. Parser类解析因子的过程中新增通过判断下一个字符为“e”进入解析指数函数的程序中,保存指数函数内部因子和指数。
  2. 新增Exp类,储存解析后的指数函数因子;并同样实现toDuoxiang() 方法,将其转化为只含有一个单项式且单项式系数为1,指数为0的多项式。
  3. DanXiang类中新增指数函数内部因子变量(这里储存为Duoxiang类型)和指数函数指数变量。并据此重写toString() 方法。
  4. 重写DuoXiang类中的add()mul() 两个方法,pow() 方法由于直接调用mul(),因而不需要修改。在进行多项式的加法和乘法时,需要先判断单项式是否含有指数项,并据此对单项式系数,指数,内部因子,指数函数指数进行运算。并将运算后的新单项式添加到原多项式中或修改原多项式中相对于的单项式。
自定义函数

思路是在读到自定义函数后之间将对应的自定义函数替换为代入实参后的表达式,再继续解析,因此只需要修改表达式解析部分,而不修改展开部分。

  1. 新增Setfunction类,在读入需要解析的表达式之前,读入并保存自定义函数的定义式(分别保存形参(ArrayList保存)和函数表达式),同时,其中的getit() 方法(传入ArrayList形式的实参)负责将函数表达式中的形参替化为实参并将转化后字符串返回,之后替换到原表达式的位置。
  2. 新增Function类,储存Setfunction类返回的替换换形参后的函数表达式,并通过setExpr() 方法将字符串再次解析为Expr类型的表达式;并同样实现toDuoxiang() 方法,实际就是调用setExpr()返回的表达式的toDuoxiang() 方法。
导函数

思路借鉴了实验中的方法,具体来说,在表达式,项和因子类中分别实现了求导方法,,结果均返回为Expr类型。

  1. Expr类中,对expr中的每一个term求导,得到的表达式转化为仅包含一个表达式因子的项,再把每一项添加到一个新的表达式expr中,返回该expr即为求导结果。
  2. Term类中,需要用到求导的乘法法则,多个相乘的因子组成的项在求导后会转化为包含相同数目项的表达式,每一项为原项中的一个因子转化为其求导后的形式。
  3. 表达式因子和自定义函数因子的求导都是调用Expr类中的求导。
  4. 常数因子和变量因子的求导非常简单,这里不详细说明。
  5. 指数函数因子的求导需要用到求导的链式法则,在本身求导后的同时还要乘上指数函数内部因子求导后的结果,之后调用Duoxiang中的mul() 方法即可得到求导结果。

其他方面

  1. 由于多层括号的出现,指数可能会超出int范围,因此在第二次作业中,将DanXiang中的指数转化为BigInteger类型;在第三次作业中,将代码中出现的几乎所有数据都转化为BigInteger类型。因为两次修改都发生在主要架构和测试结束后,因此代码进行了大量的修改,同时发现java在运行时会将未初始化的int型数据初始化为0,而BigInteger型数据却不会,由此导致在刚修改时代码无法运行。
  2. 自定义函数在替换形参时需要注意避免将exp中的"x"替换,处理方法是提前将exp转化为其他无关字符,在替换形参后再转化回来。
  3. 关于优化:第二、三次作业的优化都主要集中在是否要提出指数函数内部表达式因子的系数的公因数上。在经过具体分析后,发现在不同情况下,提出公因数有时不仅无法作到优化,甚至会负优化。本人在尝试遍历比较长度的算法后,发现其耗时过长,尤其是寻找公因数一步,因而最终作罢。最后的优化版本仅当各项系数相同且经过比较提出系数后最终结果会变短的情况下提出公因数。

代码复杂度分析,同样以第三次为例

这里由于篇幅原因只展示与第一次相比变化较大的部分。
请添加图片描述
请添加图片描述
除去与第一次作业的共性问题,复杂度较高的方法主要是Duoxiang类的add() 方法,主要原因是判断两个单项式的指数函数部分及指数部分相同占用了较多的篇幅。

遇到的bug及第二、三次作业整体感悟

  1. bug的发现与修改
    在第二次作业强测,我的程序暴露出了三个问题:
  • 首先是忘记了对在读入待处理表达式前读入的函数定义式进行预处理,没有除去其中的空格和换表符,可能会导致形参读取错误。修改方法是之间借用第一次作业的预处理方法即可。
  • 其次是在函数形参替换时,没有考虑到应当先替换x,导致出错。修改时只需要在替换前加个判断,先替换掉x即可。
  • 最后是在运算过程中采用了先运算后合并的策略,导致运算过程的复杂度过高,尤其是出现多个次方迭带时,后续在设计上改变了这里的思路。
  1. 整体感悟
  • 首先是认识到了合理架构在迭代中的重要性,由于在第一次作业就采用了梯度下降的做法,在后面的几次作业都没有进行大规模的修改。
  • 另一方面,是在设计过程中应当具有前瞻性,例如关于指数的取值范围超出int范围的问题,应当在第一次作业过程中就猜想到后续会有这样的要求,这样就可以避免大规模的修改。

发现别人程序bug所采用的策略

主要采取了两种方式:

  1. 直接阅读对方代码在某些关键处的设计,例如在多项式运算过程的深浅拷贝、指数是否使用BigInteger类型,在带入形参是是否调用解析函数,且解析函数是否具有读入BigInteger类型数据的能力等
  2. 通过评测机产生大量的相关数据,进行黑盒测试。之后在对数据进行具体分析,难点在于评测机的数据构造方式。

可扩展性评估

由于在架构中采用了梯度下降的递归算法,因此可以较为轻松的添加其他表达式元素,例如加入三角函数等只需要在不改变原来架构的前提下添加一种新的因子,整体的可扩展性较好。

未来方向

我认为由于第一单元大家的java基础普遍较差,因此第一单元的考核可以以实验的形式为主,在后续单元强化同学们的独立架构能力,而在第一单元重点让同学们体会面向对向的思想,在有样板的前提下也有助于同学们形成良好的代码风格。

总结

对我来说,第一单元的学习是oo课程的一个良好开端,尤其是给了我许多教训,例如对于后续要求的预判,在设计上要考虑全面,乃至java语法对于不同容器的使用和深、浅拷贝的应用等,让我能以更好的状态投入到后续单元的学习之中。
最后,希望我的头发不要掉得太快。

  • 29
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值