OO_Unit1

第一单元作业总结博客

第一单元的作业总算是全部结束了,如果用一个词来形容我的三次作业,那就是“灾难”。我的oo学习历程从开始就很灾难,假期因为自己犯懒,只配置了工具链,看着群里的大佬们讨论的热火朝天,我也只是暗自愧疚了一秒钟几分钟。我第一次真正入手写java程序,就已经是2.27,也就是开学前一天了(当然也有我以为第一次作业在pre结束后才开放的原因,曾经的我是那么天真)。当我面临第一次作业和pre啥也没做,自己啥也不会的双重考验时,我的内心是崩溃的(此处省略一万字)。还好助教团队心慈手软,给第一单元提供了预解析读入模式,给作业的难度降低了几个层级,也让我这个人有多大胆,oo拖多晚的菜鸡不至于第一周就寄。(然后一直信誓旦旦的讲要重构的我,也一直把这个任务的ddl无限延长了)但灾难归灾难,还是要好好反思,痛定思过,不让这样的悲剧再重演(坚定)

三次作业分析

  • 第一次作业

    俗话讲“万事开头难”。我大约花了一下午的时间学习处理字符串,将中缀表达式转化成后缀表达式,最终毅然决然的选择了预解析读入模式。在一番思想斗争以后,我大致理出来了解题的思路:因为预解析模式是一步一步走的,每一步的操作数可能是单个数字与变量x,也可能是上面某一步的结果,那么我尝试不用人类的思维用机器的思维去模拟解题过程,找到了最方便的途径:用一个容器存放每一步操作以后的结果,譬如某一步是

    f1 mul 2 x

    那么就在容器的第一项存一个2*x(当然存什么要基于具体的方法),后面可能遇到诸如

    f2 add x f1

    这种就将f1,即容器中的第一项取出来,再进行计算。这样我的操作也可以根据操作符分类,分成add, sub, mul等。当然也有一些需要特判的

    f1 neg x

    这种操作数只有一个,那就需要特判一下,否则直接拆分的话,容易抛异常。另外还有更反人类的,这个bug居然到第三次才测出来,而且本意也不是直接hack这个bug,而是sum函数的反常行为,导致直接预解析出一个

    f1 0

    连操作符也没有,直接按空格拆分,又会数组越界,抛异常。(这里还是想夸一夸java语言的异常机制,比C语言直接卡死在那强了不知道多少倍,可能真像高老板说的,java这些高级语言的异常机制就是为了我这种垃圾代码制造商准备的)
    输出的结果,只要把容器中的最后一项取出来就可以了。与其说是化简表达式,倒不如说是根据预解析一步一步生成的表达式。
    第一次作业的大致思路就是这样,具体方法的实现和优化到后面再分析,预解析模式也确实让作业难度降低了几个八度,也导致了周四的研讨课我坐了两节课的牢(悲

  • 第二次作业

    第二次作业做的我十分愧疚,具体原因是由于强大的预解析,我用了大概15分钟就写完了。神马自定义函数,sum函数都与我无关,只需要在原有功能上加一个sin函数和cos函数就行。而这两个函数可以按照pos和neg处理(因为都是一个操作数),只需要在外面套上一层括号就行。评测的时候,中测出了一点问题,后来我仔细读题,发现sin和cos只能套幂函数和常数或者单变量因子,而我的pow实现恰恰会展开所有的乘方运算。然后出现了以下错误:

    sin(x*x*x)

    虽然在数学上,这玩意也可以叫幂函数,但不符合该题的题意。于是我进行了第二次特判:如果底数是x或者+x或者-x,就不要展开,不然全部展开。
    于是第二次作业就这么被我鬼魂过去了。。。。。。

  • 第三次作业

    第三次作业依旧是依靠强大的预解析,甚至一度怀疑自己不需要改(
    不过还是出了一些小问题的。比如题目新规定sin和cos可以套表达式,但必须套上双层括号。暴力的做法就是把所有东西全套上双层括号,因为因子也可以是表达式嘛。但本着良心发现,我还是进行了优化。比如我发现我第一次写了一个方法,判断是否是简单因子(像x, 2, +x这种就算,x*x就不算),虽然在第一次没用上,但没想到在第三次用上了,直接调用方法,遇到sin和cos,如果是简单因子,就套上单层括号,否则套上双层括号。
    另外的问题出在优化上。我在乘法运算中做了符号优化(不做的话也不符合格式),大致的思路就是找负项的个数,奇数的话就在前面加负号,否则就不加。比如下面这个式子

    -1*-x+2*-x+-3*-x*-x

    就会优化成这样:

    -1*x-2*x-3*x*x

    然鹅这种暴力的优化方法在第二次和第三次作业就会出现问题。第二次还好,本身不存在sin和cos中出现表达式的情况,也就没有符号优化一说。但第三次不一样,sin和cos中的符号不能和外面的符号同日而语,但对于上述方法,就会出现很诡异的现象。譬如下面这个式子:

    -x*sin((-x+2-3+4*x))

    就会被优化成这样:

    -x*sin((x+2+3+4*x))

    因为sin和cos中无论是简单因子还是表达式,都会被看作是整体一个因子。如果说这两个式子是等价的,恐怕要被高中数学老师打死。再加上我对表达式的各个项做了一些小小的操作(这个后续再讨论),像这种式子:

    sin((x+2))*cos((x-1))

    就会出现更加恐怖的现象。(具体请自行揣测,我就不拿出来丢人现眼了)

    在经过另一番挣扎后,我做出了一个艰难的决定:不优化。既然不能精准防疫,那就一刀切。既然不能精准优化,那就不优化。我的思路是:遇到所有的sin和cos,里面的东西就原封不动的输出,计算符号时,也不考虑他们。那又怎么优雅的判断是不是在三角函数里面呢?考虑到括号只会在三角函数中出现,我维护了一个栈,用来记录左括号和右括号的个数,如果左括号个数大于右括号,就说明还在三角里面,所有优化操作均为无效;如果相等,即栈为空,则进行优化(说到这,我又想起了一年前数据结构一个括号匹配的题,我因为数组开的不够大debug到一点
    就这样,我的第三次作业也被我这样蒙混过关。。。

程序结构

讲真这个非常不想分析,因为我可能是全6系近5年唯一一个一个主类到底的选手。也不是完全不会面向对象,只是第一周完全抱着摆烂的心态去做,能交上去算合格就是万事大吉,后两周也就疏于重构了。好在我还没有一烂到底,在主类里还用了好几个方法,倒也不至于一个主函数好几百行。最后只有两个方法超过了60行,代码风格也拿了60分。正所谓这个程序现在只有我和上帝可以读懂,可能过了几个月,就只有上帝能读懂了...

  • 类图

    真的还要画类图吗?何必自取其辱

    缺点自不必说,可读性,可维护性都烂到根了,逻辑也是相当的混乱,我能想象出互测环节看到我代码同学那怀疑人生的眼神。
    优点至少得编一个吧简洁,方便,不用在各个类之间切来切去(狗头)

    简介一下各个方法和属性的功能

    • results自不必说,存放各个步骤结果的容器,最终的答案也存在里面
    • changeSymbol: 传入一个表达式,改变它的符号(正变负,负变正)。用在两个地方,一个是操作符是neg,另一个是减法,我在两个式子之间加了减号,并且将后面的式子变号。当然变号不是简单的加减号,而是优化过的,优化过程也很艰辛()
    • powCal: 指数运算。底数和指数都是字符串,因为指数可能存在“+000”的情况。指数运算直接用循环,如果指数是0直接返回1(字符串),否则循环调用乘法。
    • isBasic:是否是简单因子,这个前面有介绍过
    • isPositive:是否是正数。如此表述不是很明确,因为我是这样判断的
      return ((isBasic(str)) | str.charAt(0) == '+');
      可以看出,这个并不是判断是否是正数,而是判断前面是否有正号。这一方法,也是为了后续优化服务的。
    • cal:这个就是全场最恶心人最核心的部分:乘法,主要在于多项式乘多项式。再加优化部分,这个部分我也做了权衡,是最后一起优化还是在乘法部分优化(因为需要优化的诸如-1*-x这种都是在乘法部分出现的)。最后我选择了在乘法部分一起优化,因为这样就能保证每一步操作(包括加减法)的操作数都已经是优化好的,本操作只需要做好该步的事情,而无需担心前面传过来的因子会不会导致表达式出错。这也导致我的cal方法直接干到了100+行...
  • 各个方法分析

    • cal
      这一大段代码的核心就是cal了。我解决多项式的问题参考了以下这个公式:

    所以思路也很简单,就是把第一个式子中的每一项和第二个式子中每一项相乘,再加起来,就万事大吉了。但实现起来就有点点困难了,如何优雅的按加减号拆分表达式,让它成为包含很多项的数组呢?第一个想到的肯定是正则表达式,但且不说我当时正则表达式都不会,即使拆分了,项前面的符号也不能保留下来。于是我苦苦思索,在一节选修课上想出来一个惊天地泣鬼神的阴间绝招:在每个加减号前都加一个空格,这样再用split(" "),就可以完美的拆开各个项了,还能保留符号,简直excellent!注意我提到是加减号而不是正负号,因为诸如-1*-x*+2这种可万万不能加空格,否则就算把项拆了。加减号起的是连接作用,而正负号是跟着因子起修饰作用的,虽然他们两对长得一模一样,但操作的因子本身也是前面生成,于是可以在add,sub这种直接带上空格,而pos(虽然pos我压根没处理),neg这种就不要空格了。
    然后就是优化。(要不是说-1*-x这种形式是错的,我才不会优化它,bug都是优化出来的)。我是一项一项优化的,也就是说优化的时候各个项还在数组里,最后拿一个StringBuilder一项一项拼起来就可以了。优化的原理请查阅七年级上册数学课本“有理数的乘法”,大致就是扫描整个项,查查有多少个负号,把这个数记下来,然后把所有的负号全部干掉,如果这个数是奇数(模2等于1),就在前面加个负号,否则就加个正号。最后一项一项拼,别忘了每个加减号前面还要有空格(因为它还可能作为后续的操作数),再删掉第一项最前面的正号,大功告成!
    cal方法因为涉及多项式相乘,免不了要进行双层循环,复杂度达到了O(n^2),另外优化的过程中考虑到各种奇奇怪怪的因素,最长的if-else分支达到了4个,总行数达到了100多行,说他一句又臭又长不过分吧(悲
    但毕竟是核心代码,就凑合看了。改进的空间大概就是把优化再拎出来成为一个方法,在每个cal之后直接调用此方法,但我也没有这么做

    • 主函数
      唯二两个因为代码超长,导致我代码风格被扣掉40分的,除了cal函数以外,就是这该死的主函数。本着“高内聚低耦合”的原则(我自己都不好意思说出来),我的主函数没做什么具体的工作,只是分析每一个步骤的操作符,用到操作数,把任务下发下去。但细化任务也是个大工程,大致流程如下:

    这里存在一步判断是否是简单因子的部分,看似对相同的操作符执行相同的操作,实则不然。诸如对mul,简单因子可以直接执行
    > num1 + "*" + num2

      而对于表达式,则需要调用cal函数来计算。  
    

    看这个流程图就知道,这个函数一定不会短。首先对操作数和操作符总数判断就有3个if-else分支,后两个还要对操作符进行switch-case判断,最多的达到了5个分支(但我也实在想不到有什么改进的办法,毕竟操作符就那么多嘛)

    • powCal
      最后再说一下pow运算。pow运算比较简单,就是循环调用乘法。我一开始的思路是定一个字符串1,然后开始指数为上限循环相乘,这样就无需特判指数为0的情况,但缺点是最后多了一个1*,比较的恼人,所以我干脆特判了一下,如果指数是0,直接返回1;否则将字符串初始化为底数,再进行指数-1次乘法。举个例子,比如要计算x**3,那一开始就将要返回的字符串初始化为x,再进行3-1=2次乘法,就可以了。
      这里还涉及一点小小的技巧,就是传进来的指数可能不是规规整整的整数。你永远想象不到做测试的人有多阴险,指数可能存在+9,+009,000009的情况。这时候如果再用charAt(0),就会得到一些奇奇怪怪的东西。这就要派出我们的神器——Integer.parseInt(String),参数是一个字符串,返回这个字符串的数值。无论是带正负号,还是前导零,这个函数全都能解析出来。但好用也不能滥用,如果你传一个xyz进去,就会抛异常。在循环变量上套一个这个,就完全不担心指数的形式多么阴间啦~

      i < Integer.parseInt(num2) - 1;

      (分享一下我的nt经历。我只考虑到非0情况的前导零和正号,而特判却默认进来的一定是“0”,结果是第二次作业被揍得鼻青脸肿,抱头痛哭)
      其他的函数就都比较直观或者简单了,在这里不再赘述了。

  • oo度量

    话不多说,直接上图

bug分析

终于到了我们最喜欢的debug环节

  • 第一次作业

    开头说过,第一单元作业做的很“灾难”,那么第一次就是“难中难”。本身时间不够(从周四才开始设计),再加上水平菜的一批,开始就是摆烂的心态。周五晚上倒数第二次提交,只差第五个弱测点没过,当时一心只想拿基础分的我,对着代码一通乱改,居然鬼迷心窍的认为pos是用来抵消neg的,瞎改了一通以后,交上去居然十个点全过了,也就没再管它。
    结果就是,我第一次连互测都没进,到了周日晚上,我总结出一下bug:

    pos处理的不对,这个纯属我自己作死,虽然不知道第五个点错在哪。pos压根不用管

    如果某一步的操作数是fn,我的做法是取出这个字符串的第二个,再套上parseInt,作为要取出来的容器中的操作数,比如f4,用f4.charAt(1)就能得到“4”,但问题在于f15这种就不行了,于是我改成了

    int start = num1.indexOf("f");
    int end = num1.length();
    int getNum1 = Integer.parseInt(num1.substring(start + 1, end)) - 1;
    num1 = results.get(getNum1);
    

    就可以了

    最后一个就是对题看的还是不够仔细。像2**5这样必须得展开,但我没展开,还大言不惭的去问助教,想来是如果底数是简单因子,就直接套上指数,否则经过powCal的运算。后来我干脆一视同仁,无论啥因子都进一次powCal,就不存在上述问题了。

    因为第一次没有进互测,就没有互测的bug。果然只要不进互测,就不会被hack到。

  • 第二次作业

    第二次作业的bug前面已经提了,中测的就是sin(x**2)这种被我展开了,然后第一次OS上机的时候改正的(谁叫工训楼的电脑那么垃圾),强测和互测都是指数+000的情况,增加不超过十个字符就改正了。
    然后我的不败金身也就这么破了

  • 第三次作业

    第三次在中测倒没遇到什么阻碍,甚至有一个很大的bug都没有测出来(果然他们说的中测都是骗你的,我P5中测也过了,课上不是照样挂了)但当时十个点都过了,我把那个大bug修完,本地测试没啥问题就高枕无忧了。
    然而又被揍了。看了下输入,有一个sum(i, 99999, 1, x),这种就属于数学老师见打系列,哪有求和函数上限小于下限的?但它偏偏就是合法的,而且预解析的行为也很反常,直接弄出来一个fn 0,hjh,就这么一个小式子,给我的程序弄异常了。没办法特判吧,(如上图所示),如果没有操作符,就直接存进去。

第一次作业倒是为了改bug动了不少,但复杂度应该还是差不多的,因为没有大改。第二次第三次只动了差不多一两行的样子,对复杂度就更没影响了。

hack策略

这条我实在编不出啥了,我hack的策略就是构造一些很恶心的嵌套,以及当时困扰我的bug。因此经常出现超cost,或者不合法的数据。
所谓“乱拳打死老师傅”,我恶心的数据几乎没有恶心到别人,但我一个随随便便的sum(i,1,100,i*x)反而刀了一个人...

架构设计体验及心得体会

一直对“架构”这一词不是很理解,后来上网搜了一下,架构大概就是不同模块之间的耦合性和功能联系。如此来看,我的代码可以作为反面教材了。
也可能是强大的“预解析”,导致我后两次作业完成相对轻松,只是增加了相对简单的功能,而并没有对整体的设计进行重构或者大改。究其主要原因,可能是这门课被我学成了PO(Process Oriented, 面向过程)(下次一定

最后总结一下吧。写这篇博客之前,我内心十分纠结,要不要把自己这堆东西分享出去,因为它实在是没啥值得借鉴的部分。但我还能腆着脸,把自己的做法一点点分析出来,我都佩服我自己那恬不知耻的勇气。但成功也好,失败也罢,最重要的还是吸取教训,总结经验,为下次作业做好充足的准备。

posted @ 2022-03-26 15:44  YiWforever  阅读(40)  评论(0编辑  收藏  举报