BUAA_OO_第一单元总结

BUAA_OO_第一单元总结

​第一单元是表达式求导,难点在于表达式解析,表达式长度优化,和表达式格式检查。第一单元作为学习OO的开始,对我而言还是很有挑战的,但我做完后也是颇有收获。下面来分享一下我的思考。

第一次作业

解题思路

回想我第一次看到OO作业时,就被这从未见过的篇幅所震撼了。但经过仔细阅读后,发现还是有所思路的。

  1. 要想求导,首先就要对输入的表达式进行合理的解析。从题目中我们可以看到表达式是由若干项用加减连接起来的,所以我们求导只需要对每一项进行求导再将所有的求导结果连接起来即可。

  2. 我们可以将项设计成一个类Term,成员变量有两个,项的系数coe和项的指数index(如果项为常数则index=0)。求导方法der()可以通过改变两个成员变量实现。输出字符串方法toString()也就可以直接根据coe和index输出最简洁的字符串了。

  3. 关于缩减结果的长度,我们可以用TreeMap存储项。我之所以选择TreeMap是因为它有两个功能

    • 可以通过将index设置为key,方便检索指数为x的项是否已经存在于容器中,若存在,可以合并同类项,若不存在则将这一项加入Map

    • 可以按照key,以一定规则排序元素,使得输出结果的项的指数是递减出现,输出比较清晰,也方便检查bug。由于默认是升序输出,所以我在参数中加了新的比较器。

      TreeMap<BigInteger, Term> allTerms = new TreeMap<>(new Comparator<BigInteger>() {
      	@Override
          public int compare(BigInteger o1, BigInteger o2) {
          	return o2.compareTo(o1);
          }
      });
      

类图

​第一次作业由于作业相对容易,且我还没有初步掌握面向对象的设计思想,所以写的还是相当面向过程的,虽然定义了几个类,但真正有面向对象思想的就是Term,其余的大致相当于函数调用。这是我第一次作业的UML图

  1. MainClass依次创建了PreProcessor,TermProcessor,Term,并调用了其中的方法。首先读入了表达式,通过PreProcess里面的方法对表达式进行了预处理,随后对表达式进行了拆分,通过TermProcesser对项进行了分析,提取出信息,随后建立Term,对Term求导后将结果放入MainClass的Map中进过合并后,依次输出。
  2. PreProcessor主要是用正则表达式找出连接相与项之间的加减号,将这些加减号替换为符号‘@’,以便之后用split方法将每个项之间分开
  3. TermProcessor主要是解析项的字符串,找到项的系数和指数,找到系数就与原系数相乘,找到指数就与原指数相加.
  4. Term中有两个方法,der(),调用这个方法,将Term中的coe和index改变,来求导;printTerm()通过coe和index将该项输出

度量

本次作业代码共294行,除去空行后201行,最长类96行,最短类32行,都基本符合规范,checkstyle100分,所有方法都不超过过60行

这是所有方法的度量

复杂度分析

这是两个复杂度较高的方法

第一次作业复杂度较高的方法比较少,printTerm()的复杂度高是因为为了简化输出,其中存在大量的BigInteger的compareTo函数。(由于这个比较逻辑是按照第一次实验上所给的逻辑写的,所以我放出来代码)

public String printTerm(boolean ifFirst) {
    StringBuffer toPrint = new StringBuffer("");
    if (!ifFirst && coe.compareTo(bigZero) > 0) {
        toPrint.append("+");
    }
    if (coe.compareTo(bigZero) == 0) {
        toPrint.append("");
    } else if (coe.compareTo(bigZero) != 0 && index.compareTo(bigZero) == 0) {
        toPrint.append(coe.toString());
    } else if (coe.compareTo(bigPoOne) == 0 && index.compareTo(bigPoOne) == 0) {
        toPrint.append("x");
    } else if (coe.compareTo(bigPoOne) == 0 && index.compareTo(bigPoOne) != 0) {
        toPrint.append("x**" + index.toString());
    } else if (coe.compareTo(bigNeOne) == 0 && index.compareTo(bigPoOne) == 0) {
        toPrint.append("-x");
    } else if (coe.compareTo(bigNeOne) == 0 && index.compareTo(bigPoOne) != 0) {
        toPrint.append("-x**" + index.toString());
    } else if (index.compareTo(bigPoOne) == 0) {
        toPrint.append(coe.toString() + "*x");
    } else {
        toPrint.append(coe.toString() + "*x**" + index.toString());
    }
    return toPrint.toString();
}

main函数由于面向过程,写的比较长,ifelse比较多。所以复杂度也稍微高了一点。

关于bug

在第一次作业中,我强测和互测都没有被测出bug

第一次互测中,我发现了别人的一个bug 测试用例是

-+x*6444*1*x--x**7*x**+2184065668*x**+27313202511

他的输出是

-12888*x-29497268186*x**29497268185

正确输出是

29497268186*x**29497268185-12888*x

显然,他在处理可能出现的连续符号的方面犯了一些错误。忽略了项之前可以有简化-1的规则,这也是常犯的一个错误。无论是拆分项,还是找系数和指数的时候,我们都要小心处理符号。

小经验

  • 由于题目保证给出的测试用例都是符合规范的,我们可以大胆进行处理。比如去掉空白项,去掉项中的‘+’,和‘--’
  • 在运用正则表达式时,其实连接项之间的正负号和常数因子前的正负号有本质区别,我们可以将连接的正负号通过正则匹配换成别的符号,比如‘@’,来方便处理,还可以将指数符号‘**’换成‘^’,以免在正则中不好与乘号区分
  • 输出时可以将 x**2 替换成 x*x ,长度短了1位

第二次作业

​第二次作业的难度相比第一次可以说是质的飞跃,是我认为最有难度的一次,我也是思考了很久,在研讨课上受到某位大佬的启发,用了递归下降的思想,才磕磕绊绊写出来,但是由于对递归的理解不到位,出现了括号嵌套很多时超时的现象。

解题思路及重构

​这次作业引入了三角函数和表达式因子,使得作业难度飙升,因为这意味着会有更复杂的求导公式,且表达式可能作为项在下一层出现,这样就可能出现无限层数的嵌套,在这一点上就需要用到递归的思想,下一层给上一层返回求导结果或输出自己的字符串,上一层并不关心下一层的细节是什么,只用到下一层传上来的结果,这样想来,其实求导过程在表达式-->项-->因子这一步也就结束了。

​但是由于我的第一次作业是比较面向过程的,很难在其基础上完成第二次作业,这次我对代码进行了重构。第一次作业也不是毫无意义,我依旧采用第一次作业的方法来拆解表达式,用Map存Term实例

  • 建立一个抽象父类Factor(因子),其中定义两个抽象方法:求导der()和输出自身toString()。子类有Const(常数)、Cos、Sin、X、和Expression,这些类都要实现父类的der()和toString()方法.

  • 在这些子类中,Const的成员变量自然只有一个,Cos和Sin由于括号中只能是x,所以成员变量与X相同有两个。Expression的成员变量是一个Map,其中的因子是Term。der()和toString()方法通过这些成员变量来输出结果,具有比较低的耦合性。

  • Term的实现颇费我心思,是减小结果长度很关键的部分。我在实现Term的时候就企图将Const、Sin、Cos、X等合并,因为他们结构统一,合并只需要在意指数的变化,最终Term实例中只会保存一个Const、一个Sin,一个Cos,一个X,且为了Expression中Term的合并方便,Term中有描述其组成的成员变量coe,indexSin,indexCos和indexX。Term不是Factor的子类,其成员变量是个List,其中依次保存着每一种类型的项,Expression是单独保存的。

  • 由于求导的乘法法则:[f(x)g(x)]′=f′(x)g(x)+f(x)g′(x),一个项在被求导后很可能还要输出求导前自身的字符串,所以我们的求导方法der(),不可再像第一次一样改变实例的属性,而是只输出求导过后的结果。

  • Term的求导是很复杂的一环,因为其是有很多因子相乘所组成的,Term的求导我也采用了递归的思想。比如一个Term有四个Factor组成,为a*b*c*d,那么令f(x)=a , g(x)=b*c*d, g(x)是一个新的项,f(x)是某种因子,g‘(x)返回新生成项的导数,这样一层一层递归,就能返回Term的导数。为了实现这个功能,Term需要两种构造函数,一种的参数是字符串,另一种参数是一个list,由于求导并不会改变实例的成员变量,所以不需要考虑求导的问题.

  • 关于Expression的合并化简问题,总体思路还是和第一次作业一样,用Map的Key来决定是否可以合并,这里需要建立一个类KeyOfTerm,传入Term的成员变量,来重写equals和hashcode方法。

类图

这是第二次作业带着成员变量的主要类的类图

通过图我们可以看到,Main函数传入一个字符串交给Expression,Expression分成若干Term,Term又分成若干Factor,这些Factor中如果有Expression,那么就开始下一轮的循环,直到所有的Term中都不再有Expression,则递归到了底层,递归即将结束。

  • Expression用Map存其所属的Term,求导时用迭代器输出每一个Term的导数,合并时用key合并
  • Term中用ArrayList存每一个Factor,其它成员变量为的是方便Term的合并
  • Factor的其余子类的成员变量就是指数和系数,通过他们来求导。

度量

本次作业代码共649行,除去空行后539行,最长类266行,最短类7行,都基本符合规范,checkstyle100分,所有方法都不超过过60行。

其中Term比较长是因为Term中包含了很多的处理Factor的内容,这使得各个Factor的行数很少,主要是为了方便化简,在第三次作业中Term会进行精简

这是所有类的度量,果然也是Term和Expression中复杂度很高,也是因为这两类中包括了大量的处理化简操作

复杂度分析

以下是复杂度较高的一些方法

​这些复杂度较高的还是一些较为复杂的处理方法,包含大量的ifelse等语句,说实话我也不知道如何降低这些复杂度,可能程序中必然要包含一些相对复杂的方法才能完成比较复杂的任务,但确实复杂度高的方法难以维护、耦合度高,容易出现bug,我在第二次作业中的bug也来自于Term.toString。

​有些方法的复杂度如此之高,可能是我的面向对象思维还不够成熟,如何更好的使用面向对象方法,降低程序复杂度,也是我今后学习的目标之一

关于bug

​这次作业我犯了一个非常值得警醒的错误,我强测中的两个点和被hack出来的三个点都是因为这个bug。

​在Expression的der()方法中,我为了图方便,随手调用了好几个Term.der(),这一下子,这一层的Expression要进行三次Term.der(),下一层就要进行9次Term.der(),如果有20层无用的括号,那可就是3^20次,30多亿次调用!我写的程序直接慢过GTA5。

​我用一个临时变量将依次递归的结果保存下来,当需要引用递归结果时就调用这个变量,相当于只递归了一次,这样更改后,速度变得快多了

​递归时,如果要递归下一层,那么这一层只能实现一次递归,否则递归将以指数的复杂度持续增长,直接炸裂。比如你需要下一层的toString,那么递归一次用临时变量存起来,在这一层处调用,不要随处都写个递归 ,复杂度爆炸增长。

本次互测我自动生成了一些很长的数据hack掉了3位同学,可能由于第二次作业的难度较高,大家都会有一些问题。下面是我的部分hack成功样例

+-(+x* cos(x)** +2 +- sin(x))* sin(x) *+4
- 48-+ (- (- - -53 * +73*cos(x))- - ( -- 96 *x** +44))*97
+ sin(x)** +3*x **4*+5 *cos(x)** +36+ + -8* x *sin(x)**+4

小经验

​可以了解一下正则表达式的先行断言和后行断言,在找一些因子的时候会事半功倍。但也要注意匹配是贪心的,最好不要用否定的断言

第三次作业

​第三次作业增加了sin中可以嵌套因子和格式检查,格式检查是比较让人头痛和容易忽略部分情况的点

解题思路及重构

由于第二次思路比较清晰,架构比较好,所以第三次改动的地方比较少,有这几个方面

  • 由于Sin、Cos中可以嵌套因子了,所以Sin和Cos就不再是递归实现的最后一层了,我将原本放在Term中的处理因子的部分都转移到了 Factor的构造函数中,用正则表达式捕获sin,cos中的因子,递归建立。这块也算是一个小重构了,牵扯的地方也不少。
  • 关于化简得部分也有所调动,由于狮子的复杂,我对于化简得要求也不再那么细致,总体思路还是与第二次作业相同,但但凡涉及三角函数的项就不再进行化简了。

关于格式检查,这是这次作业中最令人头疼的一个点,经过思考后我打算这样处理问题

  • 首先先排除表达式中是否有不合法的字符
  • 单独先检查是否有空格导致的格式错误
  • 在Expression得时候检查括号是否一一对应
  • 在Term中去掉这一项的最多前面两个符号,如果去掉两个后,在因子的格式检查中还错误,那就是符号有问题
  • 将每一个因子都用正则表达式进行匹配,如果匹配不上则这个因子有问题

这样一层层下降的方法来检查格式,下降前保证这一次的下降不会出现问题,最终下降到底层因子,用相对简单的正则表达式进行判断,自我感觉还是挺合理的,但一点要注意思维的全面性,我强测的一个bug就是因为少考虑了一个点

类图

​由于架构没有发生太大变化,所以主要类的类图与第二次作业基本一致,但注意Sin和Cos能生产Factor了。这样的循环就更大了一点 最大的循环是 Expression-->Term->Factor-->Term-->Expression 直到所有的循环都到了没有下一层递归的X和Const中,递归结束。

​我新建了一个类JudgeFormat,里面保存了静态变量和静态方法,要判断哪个随时调用,如果不符合直接输出"Wrong format!"退出程序.

度量

本次作业代码共827行,除去空行后730行,最长类180行,最短类7行,都基本符合规范,checkstyle100分,所有方法都不超过过60行。

由于把Term中许多过程都交由Factor的构造器完成,Term精简了不少

这是所有类的度量,复杂度高的类都涉及到化简和递归调用下一层

复杂度分析

这次复杂度前几名的方法比上次的复杂度竟有下降,想必是我小重构后使得程序的结构设计更合理了,看来程序的复杂度也是衡量面向对象思维的标准,路漫漫其修远兮~

关于bug

最担心的事情还是发生了。

在写程序的时候我就怕某一种格式检查没有想到出了问题,结果强测错了一个点,就是因为我忘了判断表达式因子是否位空串,在加上之后就正确了

这次由于构造难度比较大,且大家可能因为第二次作业的强测完善了自己的程序,我没有查出我们房间内同学的bug。

小经验

工厂模式,真的很舒服。

心得体会

​写到这里,本次的博客也接近尾声了,其实我已经写了很久,有些累了,但在回顾了我这三次作业后,我发现我的技术增长还是很明显的,现在看来,我的第一第二次作业的架构都多多少少有些丑,在感到累的同时,回顾自己的进步还是一见挺开心的事情。

​前段时间由于其它需要写了一段C语言程序,这时我才感受到面向对象的好处,C语言不支持面向对象,也没有很多现成的类帮我实现功能,还得自己重新造轮子,指针也忘的差不多了,整体写下来代码相当混乱。(当然我只是为了强调面向对象的好处,C语言,yyds!

​学习编程就是这样痛并痛着,但是带来能力的提升是肯定的,马上就要进行下一单元了,快乐的一周就要结束了,希望我能在下一单元有更多的收获!

posted @ 2021-03-28 17:42  mjw0803  阅读(96)  评论(0)    收藏  举报