BUAA OO第一单元总结

本文详细描述了作者在编程作业中对代码结构的分析,包括预处理、表达式解析、递归处理自定义函数、多项式转换,以及性能优化的过程。作者反思了类设计中的不足,并分享了如何发现和修复程序bug的经验。
摘要由CSDN通过智能技术生成

PART1 代码结构分析

1.1 程序最终架构的类图如下

在这里插入图片描述整体的思路可以分为以下几个部分

  • 预预处理(自定义函数的定义和展开)
    首先输入了自定义函数的个数,之后先“收录”所有的自定义函数的定义式。在Function类中,我设置了两个主要的hashmap容器,一个负责保存函数名和定义式之间的映射关系,另一个负责保存函数名和形参(形参可能有多个,所以也用一容器保存)的对应关系。最后对输入的字符串进行自定义函数的展开。
    这里要注意exp中的x可能被替代,所以在“收录”自定义函数的定义式之前,我又另写了一个函数将x换为u,如遇“e“就跳过整个”exp“,算是勉强解决了问题。另外,在进行展开时,由于这种处理方法是纯粹字符串层面的处理,须注意可能会有调用时函数嵌套的问题,所以展开的函数是递归函数。
    这种处理方式在思维上容易一些,不过会承担更多的性能损失。

  • 预处理
    预处理阶段负责处理空格,\t,消除连续的加减号,并且将所有的符号转化为数字前的符号,这样就解决了可能出现的空白符和复杂的符号处理的问题。在后续的符号处理中,只有项(Term类)和数字(Num类)考虑到了符号的问题,极大的简化了后续的处理。

  • 表达式解析
    这一部分我按照课程组公众号中表达式解析的代码的思路,采用递归下降的方式完成了表达式的解析。表达式(Expr)类中有项(Term)的容器,项中有因子(Factor)的容器。表达式按照加减号划分成项,项中按照乘号划分成因子。
    “遇到的”因子有以下可能,下述这些类全部实现了一个Factor接口,便于进行统一处理。

    • 数字(Num类) 递归下降的终点之一
    • x及其幂次(Power类) 递归下降的终点之一
    • exp函数(Exp类)递归下降的终点之一
    • 表达式因子(Expr类)遇到表达式也就是遇到了输入表达式内部的括号,需要继续递归,将这个“子表达式”也彻底解析。
      在表达式解析完成之后,实质上已经没有括号了,表达式中的所有因子按照其所在的“层次”被放置在不同的容器之中,但是此时工作并没有结束,想要输出一个合法的表达式,要将在同一个项中的因子相乘,将在同一个表达式的项相加才能形成表达式。但是不同的类不便于直接进行相乘相加,所以我们需要新的类来将看似千差万别的式子同一化,再进行合并输出。
      另一个问题是表达式因子可能存在的幂次,我的处理是将表达式因子的幂作为Expr类的一个属性,默认值为1,在进行展开时先将表达式内部展开,再根据幂次进行“自己乘自己的操作”,但是要注意可能存在的深浅克隆的问题,在进行相乘时尤其要注意不能改变原有对象。
  • 表达式转换为多项式

    在解析工作完成之后,我们得到了一个“层次丰富”的全新表达式,而不是开始时输入的那个扁平的字符串了。此时我们离目标的距离就是:这时候还看不见符号的踪影。尽管表达式的每一个部分都按照不同的层次划分好了,比如(x+(x)),就会被解析为一个Expr对象,这个对象中有一个Term容器,Term容器中有两个Term,比如term0和term1,term0里有一个factors(Factor容器),factors里有一个Power对象,即x;term1里有另一个factors,这个factors里有一个Expr类对象,为什么会这样呢,因为第二个x有括号。理想情况下,即输入的表达式就没有括号,那么解析出的Expr对象内部(也就是对象内部的terms内部的factors里的任何一个factor)就不会有Expr。内部出现了Expr就意味着有括号。这样做的好处在于,我们不仅知道”这里有括号“,还非常清楚地知道括号内部有什么,而且可以容易地对其内部的内容做操作。假如只是字符串,我们也可以通过遍历”发现“括号,但是对其内部是什么就毫无所知了,更不要说进行进一步的括号展开会有多难了。
    所以,怎么才能把符号“加上”,把这个层次丰富的Expr对象转换为表达式呢。当然不能原样加回去,我们的目的是要打破括号。刚才我们分析道,式子内部的括号会以Expr对象的形式出现,那么这种对象必然是以表达式因子的“身份”出现,也就是会出现在某个term内部的factors中。那么又分为两种情况:

    • 这个factors中只有表达式因子这一个对象
    • 这个factors中又有许多其他类型的对象
      第一种情况时,我们就需要“相加”操作,第二种情况就需要“相乘”操作了。
      但是,由于我们有许多不同类型的因子,相加或相乘的可能性实在太多(比如x和3乘变成3x,x和exp(x)乘变为xexp(x))。最重要的是,乘法操作后,结果很可能就不能用原先存在的因子囊括了(假如又变为Expr对象,“层次”依然没降,且更加不好分辨),所以我们需要新的类,即单项式类(Mono)和多项式类(Poly)。单项式可以理解为最小操作单元,例如第一次作业中,单项式管理着系数和指数。也就是c*x^e的形式,在进行加法操作时,只需判断两个单项式对象的e是否相同,若相同就将c相加,不同就将两个对象都加入到一个多项式对象中(多项式中只需管理一个单项式的容器)。利用单项式和多项式类,将符号加回式子的操作就更加易于理解了。
      所以我们需要将原有的Expr对象转换为多项式,也就是要自下而上地转化为poly。对expr对象来说,要将term.topoly()“相加”,对term对象来说,要把factor.topoly”相乘“,对factor对象来说,递归终点均好转化,而expr对象要进行又一轮转化。不同的是,这次转化后,得到的多项式并不是一个”层次丰富“的对象了。
  • 多项式转换为字符串

    在经过以上操作后,我们得到了一个Poly类对象,也就是一个多项式,这个多项式是由一个Expr类对象转换而来的,而这个expr对象就是输入的字符串转化而来的。
    我的处理中在一次作业只有poly类中有tostring方法,也就是集中考虑所有情况并输出。第二次作业新加入了exp类对象,并且要求当exp内部是表达式因子时要另加一层括号,于是我这种处理方法就显现出了问题,只能在exp类中强行另加一个方法判断内部是否为表达式因子。

  • 字符串化简输出

    这一部分中只是以防万一进行一些可能存在的空格或者连续加减号的处理。
    最后输出答案。

PART2 基于度量的代码分析

类复杂度分析
在这里插入图片描述
可以看到Function类中由于采用了递归函数而导致性能的损失。Poly类中的复杂度很可能来源于集中处理字符串输出导致分支数目过多。今后在处理类似问题时我需要更加重视性能问题。
方法复杂度分析(在此节选有问题的部分)
在这里插入图片描述
在这里插入图片描述
可以看到,问题果然集中于上述分析的部分。过多的分支数目或者频繁地调用循环都会造成复杂度过高。
下面是代码整体规模分析
在这里插入图片描述
类的内聚耦合分析:在本单元作业中类的数目并不少,并且各司其职。我认为在我的设计中,有几个地方并没有处理好内聚耦合的关系。比如:toString函数直接设置在Poly类中,集中处理本应该是其他类处理的内容,不仅造成了许多后续问题,不具有足够的可扩展性,还大大增加了Poly类的复杂度。

PART3 架构设计体验

在三次作业中,我并没有经历大的重构,不过也遇到了不少问题。

第一次作业

第一次作业的要求相对简练,在阅读了很多同学和学长的思路后我选择了利用Mono类和Poly类来进行处理。大致形成了上述架构。

第二次作业

第二次作业新加入了exp函数和自定义函数。自定义函数的处理我选择在字符串层面进行。基于之前的分析,这种处理方式思路简单但是会付出性能方面的代价。而exp函数的处理相对更难。最开始我选择先将exp内部解析为Expr对象,再将Expr转化为Poly对象并转化为表达式存在exp对象里。这种方法思维简单,但却是不可行的。这样处理后,在面临“相加“操作时判断两个exp对象是否在算数层面上相等是相当困难的。但假如不在相加时进行判断合并,很可能出现tle或mle。在经过研讨课与同学们的讨论后,我将exp内部改为存储poly对象,这样相加时就可以利用两个exp内部的表达式相减结果是否为零来判断两个exp对象在算数层面上是否相等了。

第三次作业

第三次作业相比前两次略容易一些(虽然我写出了bug),新增了自定义函数定义可用已定义的函数以及求导。求导操作与拆括号的思路很类似,也是从下而上随时合并的,但是规则有所不同。我在因子类,term类都加入了求导方法,并且根据不同的规则进行不同的操作。需要注意的是可能出现的深浅克隆问题。

自定义迭代

x的幂次中可以出现m或者只含m和数字的表达式
对于Power类来说,需要将原有的数字属性变为Poly属性,同时需要重写Power对象的求导方法。由于求导之后,m可能不仅会出现在x的指数中,所以需要新建类来存储m有关的属性和方法。对于Mono类来说,原有的存储x的指数的属性会被改写为Poly对象,而该poly存储的多项式会由新的类的对象和Num对象构成。

PART4程序的bug

前两次作业我非常幸运地并未在强测互测中被找到错误,但是在进行中测提交和自己检查的时候还是发现了不少bug。

  • 第一次作业

    第一次作业初次提交的时候没有考虑到整数溢出的问题。后续对于可能溢出的属性均用BigInteger存储

  • 第二次作业

    第二次作业中我写出了一个令自己印象深刻的bug,同时这个bug也充分体现了在一个类集中输出的弊端。
    由于开始时我没有意识到exp内部只有因子的情况下才是单括号(其实也就是因为表达式因子自带括号),并且临到中测开始才意识到,于是手忙脚乱地进行修改,开始时我的判断方法是exp内部的poly对象中的mono容器假如size为1,那么就看mono中管理的系数、exp、指数是不是有两个为“空”,对于整数,就是exp内部为零和指数为0。但是这样写的问题在于,假如exp内部是exp(0+0),在我的处理方式中,exp判断时就会认为exp内部不是因子。后续我在进行相加和相乘操作时就随时判断系数是否为零,若系数为零就立刻“丢弃”

  • 第三次作业

    第三次作业中在强测和同学们的帮助下发现了两个bug,而且两个bug都体现了我迭代时的思维漏洞

    • 第一个bug出现在克隆expr对象的方法,由于我处理expr的幂次时是采用一个属性管理,但expr的构造函数并没有这个幂次。幂次默认为1,如果不是有另一个方法去修改。这就导致在写克隆函数时我忘记了expr的幂次属性,最终导致了bug的产生。今后在编写构造函数时,我将尽量把所有的属性都加上,不能因某些地方的便利使程序产生漏洞。
    • 第二个bug在于预预处理展开自定义函数的时候。由于我进行的是将字符串进行暴力替换的操作,这就导致在第三次作业增加“自定义函数定义式可以使用其他已定义函数”的功能之后会出现一些情况下实参中的xyz会被替换为其他实参。比如f(y,z)=yz,那么替换掉第一个形参会变为f(zz,1),在这时再对式子替换(z换为实参“1”),就会产生f(1*1,1)的情况。 于是我将函数定义式展开时先把函数定义式中的u(x)、y、z换为a、b、c,展开后再把a、b、c换回去,最后将处理好的定义式存起来。
      这样虽然也修好了bug,但不免让人感觉有些勉强。我认为字符串层面处理自定义函数的方式有许多难以避免的不利之处。这是我最初确定思路时没有想到的。
  • 结合之前图示的方法复杂度和规模可以看出,复杂度和规模更大的方法更容易出现bug。我认为可以从思路层面上尽量降低实现功能的方法的复杂度和规模。

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

在互测环节,我会首先阅读他人代码的大致框架和思路,观察与自己思路的异常点,并且注意自己编写程序中发现过的bug和容易出现错误的位置。之后再进行全面的测试,尽可能涉及到数据的各种可能性。
在互测环节中,我只找到了他人代码的一个错误,用的就是上述方法。

PART6优化

我所做的优化并不多,主要在于exp间的合并、单项式间的合并以及系数为0或1,指数为0或1的情况。
exp间的合并是通过将两个exp相减判断是否为零实现的,单项式的合并需要判断exp和指数是否均相同,系数和指数的化简均在输出环节通过条件判断完成。
这些化简均符合文法条件,所以并不会造成正确性的损失。

PART7心得体会

在本单元的作业中,尤其是第一次作业,我遇到了不少思路上的困难。阅读了许多博客和资料之后,我对于递归下降和层次化处理的理解加深了很多。正如老师所说,OO课程并不是一门完全的编程课,而是一门教授思想的课程。在不断地尝试和碰壁中,我逐渐体会到了层次化的逻辑清晰。类与类各司其职,实现各自的功能,顶层处理只需调用各个功能。不同的类有所交流,但都是确切需要的,这种交流也并不会破坏类内部的功能的封装性,使得代码整体更易于理解和进行扩展。
在一次次迭代的过程中,我会非常直接地感受到对于不同的处理方法、迭代的工作量并不相同。在第三次作业中,我发现字符串层面进行自定义函数展开就会面临很多可能存在的bug。所以以后在进行设计时,我会更加注重代码的可扩展性。

PART8未来方向

在本单元的学习过程中,我得到了来自助教和同学们的很多帮助,真的非常感谢大家。
我认为OO课程的设计已经非常合理了,但是尤其是第一周的作业还是有些令人感到措手不及。对于递归下降的讲解我个人认为已经非常明晰了,如果要提建议的话可能会建议对于后续的拆括号的处理再更多的进行思路上的引导。

  • 10
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周铭坤-22373193

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值