第一单元实验总结 | TrickEye

第一单元实验总结 | TrickEye

基本情况部分

  • 这篇帖子为什么会在这?
    • 这是北航计算机学院面向对象构造与设计2022春季课程第一单元的总结博客。
  • 本次作业的要求是什么?
    • 消除复杂中缀表达式的非必要括号,尽可能在恒等的前提下缩短表达式长度。
    • 表达式含有的字符集为:

个人代码结构分析

个人的解决办法基本上实现了输入解析和计算化简的解耦合。读取和解析部分由软件包expr完成并返回结果给main,计算部分由软件包calc完成并返回给main。

以上又可以分为几个子任务,每一个子任务之后都有一个可以被检视的返回值提供给main(调用者)。这无疑方便了调试过程。

  • 图1:完成作业要求的流程图
  • 首先调用官方包完成输入(Normal Mode)。
  • 由递归下降法解析得到一个最终的Expr(expr.Expr)类对象,这个对象代表了整个式子。
  • 通过递归地调用Expr, Term, FactortoParsed方法,得到一列形如预解析模式输入的字符串,完成预解析职能。得到的一系列字符串由parsed容器保存。
  • 使用process方法,对parsed中所有的字符串建立表达式树,返回最后的一个node,作为表达式树的根节点,代表整棵表达式树。
  • 使用getValue方法递归求代表根节点的值的表达式。
  • 使用化简和输出方法完成输出。

  • 图2:个人代码类图(有删改)
  • 其中,输入、解析、转换成预解析形式的职能由expr包完成,计算、化简、输出职能由calc包完成,两个包体之间完全解耦合

概念和方法解释:

  • 表达式树
    • 由观察得到,本次作业的所有运算符都是至多二元的。抽象的说,每一个运算符通过某种规则操作两个Expression,并通过数学含义,得到一个新的Expression。
    • 因此将表达式抽象成为一棵树是合理可行的。
    • 这棵树的每一个节点都代表一个运算符(非叶节点),或者一个不可再拆分的基础项(叶节点),每个非叶节点有至少1个,至多2个孩子。
    • 每个节点都应该有getValue()方法来得到这个节点代表的值
  • 递归下降、符号处理和自动机
    • 为什么把这三个东西放到一块呢?因为这仨恰好是我完成此次作业的时候遇到的迷茫、难点和解决方案。

    • 第一单元课下实验提供了一份使用递归下降法来处理表达式的代码,当时的parser, lexer模型是我递归下降的启蒙,虽然第一次作业时我看不懂,采用了字符串替换和逆波兰表达式法来解析表达式,但是付出了被找出两个bug的惨痛教训。

    • 因此第二次作业我就开始了用递归下降法重构代码的工程,而符号处理就是我紧接着遇到的问题。(也是我第一次讨论课在荣老师讨论课上提出来的问题)一个表达式前面可能有许多个连续的正负号(带符号整数,项,表达式前面都可能有符号),我们不见得知道哪个符号是谁的,这对解析带来了不小的麻烦。

    • 但是感谢有限状态自动机,感谢数学,后来我想清楚的一个道理是:符号归属于哪个因子不应该影响整个式子的数学含义和代数意义上的值。因此完全可以采用贪婪匹配法:只要正在解析的这个项有可能有符号,我们又遇到了符号,就认为这个符号是我们正在解析的这个项的符号。这样的话,一个有限状态自动机就初具雏形了。

    • 图3:用于解析表达式的有限状态自动机,(有修改,功能不一定正确完整)

    • 关于符号处理这方面,我观察到有的同学在第三次作业仍然采用了替换连续出现的正负号。虽然这样做在给定的规则下似乎是不可攻破的,但是我仍然不认为这是一个好的做法。这样的作法一旦加入了错误格式判断就会出大问题。(但是非常可惜最终还是没有引入Wrong Format错误。

  • 为什么要预解析?
    • 笔者从第一次作业开始,都是先完成了预解析模式的功能实现(也就是根据预解析模式建树、计算),随后再完成了从普通表达式转换成预解析模式的功能实现,因此两个功能部分解耦合。
    • 使用预解析从某种程度上说,也算是采用了化归的思想,将表达式中的所有元素都视作预解析模式中的一项,这在第二次作业加入了自定义函数、求和函数之后为笔者避免了诸多字符串替换等不必要的麻烦。
    • 预解析函数接收一个字符串ArrayList容器,在执行过程中会直接更改这个容器,同时返回一个字符串,代表要解析的这个项在预解析模式中的名称,以后称之为此项的形式名称。总地来说,预解析的实现方法是:递归地调用该项中所有的子项的预解析方法,存储这些子项的形式名称,根据当前项的数学含义链接这些形式名称,并返回这个项的形式名称。

代码复杂度(基于第三次作业)

class Average Operation Complexity OCMax Weighed Complexity
homework.calc.Base 1.529412 5 26
homework.calc.Complex 2.6875 9 43
homework.calc.Expr 2.588235 7 44
homework.calc.Node 2.411765 12 41
homework.calc.Tri 1.55 6 31
homework.expr.Expr 2.333333 5 14
homework.expr.ExprFactor 2 3 6
homework.expr.Function 1 1 3
homework.expr.FunctionCallFactor 2.333333 4 7
homework.expr.Number 1 1 4
homework.expr.SumFactor 2.333333 5 7
homework.expr.Term 2.8 7 14
homework.expr.TriFactor 2.666667 4 8
homework.expr.VariableFactor 2 3 6
homework.Factory 2 4 8
homework.Lexer 2 4 8
homework.Main 3 4 6
homework.Parser 4 10 20
Total 296
Average 2.192593 5.222222 14.09524
  • 可以看到,计算时的代码复杂度比解析的复杂度高多了,这是因为calc包中的一个类又承担计算,又承担化简,如果把计算和化简写开,或许复杂度会好一些。
method Cognitive Complexity Essential Cyclomatic Complexity Design Cyclomatic
homework.Parser.parseFactor() 19 6 9 10
homework.calc.Complex.combinable(Complex) 16 9 7 9
homework.calc.Expr.shorten() 16 1 7 8
homework.calc.Tri.toString() 13 1 5 8
homework.expr.Term.toParsed(ArrayList) 13 5 7 7
homework.calc.Complex.mulItem(Item) 10 4 7 7
homework.calc.Complex.toString() 10 5 8 9
homework.expr.Expr.toParsed(ArrayList) 9 2 4 5
... ... ... ... ...
Average 1.7555555555555555 1.525925925925926 2.0148148148148146 2.325925925925926
  • 这里选取的是Cognitive Complexity最高的几个方法,可以看到主要还是Parser类的解析方法(也就是上文中提到的状态机的主要实现部分)

王婆卖瓜和自我批评

  • 王婆卖瓜主要是针对第二三次作业。我认为:我的第二、三次作业的架构比较规范,一方面严格根据表达式文法建树,另一方面也兼顾了准确性和化简。第二、三次作业在正确性上没有出任何问题,第二次作业强测100分,但是第三次作业优化沿用了第二次的逻辑,因此没有把性能分拿全,略有遗憾。
  • 自我批评方面,第一次作业写的实在有些臭。没有采用递归下降法,而是字符串替换,这产生了一个隐形错误,(明明用递归下降就可以规避的)此外也犯错误采用了parseInt而不是BigInteger导致用了BigInteger也被爆数据了(悲
class Average Operation Complexity OCMax Weighed Complexity
Factory 4.555555555555555 12.0 41.0
Item 2.9 17.0 29.0
Main 1.5 2.0 6.0
Node 1.894736842105263 9.0 36.0
Poly 3.0 6.0 36.0
Total 148.0
Average 2.740740740740741 9.2 29.6
  • 以上是第一次作业的类复杂度。第一次作业写的实在没有任何被学习的价值,就自己留着吸取惨痛教训,不放出来了嗷

Hack与被Hack

作业 情况
hw1 发起0/7 受到6/23 呜呜呜呜呜呜
hw2 发起3/28 受到0/27 哈哈哈
hw3 发起1/7 受到0/29 哈
  • 经验总结

    • 只有努力变强,才能没有bug,才能找别人的bug
  • 被找到的Bug

    • 这里主要还是就着第一次作业来讲:
    • 第一个Bug是大整数,上回说到,用了parseInt而不是BigInteger的构造器(中测Debug的时候也没有改彻底),因此只要给我一个长整数我就傻了。不过这个Bug也还算好修
    • 第二个Bug是字符串替换。我的第一次作业策略是替换所有的(+-)(\\d+)\(0(\1\2\),这本来是一个恒等变形的,但是错误的使用了String.replace方法,并错误的以为这个方法是只对首次匹配生效(实际是对所有匹配生效),因此被样例-3+-345卡住,替换成了(0-3)+(0-3)45
    • 以上都不是什么有共性的bug,所以我在提交的时候完全没有考虑过测试这些点。
    • 这倒也说明了只要你的代码有bug,就一定会被热心网友们整的,不要心存侥幸。
  • 找到的Bug

    • 第二三次作业都有成功hack入账,他们分别是:

    • hw2-1没有考虑三角函数后的指数

    • 0
      sin(-10)**+2
      
    • hw2-2无脑替换了x**1为x

    • 0
      x**10
      
    • hw2-3这个不知道怎么错的,估计是0-x处理失误

    • 0
      (0-x)**+2
      
    • hw3-1 sum含有BigInteger

    • 0
      sum(i, 9999999999999998, 9999999999999999, i)
      
    • 策略方面,第二三次作业吸取了第一次作业被干的教训,明白了普普通通的数据是干不了人的。因此写了数据生成和测试脚本,生成了许多有攻击性的数据,在加以手工测试一些有意思的点,总的来说,hack还是很快乐的。

  • 但是还是有别人找到我没有找到的Bug,所以还要努力变强!

心得体会

作为与OOP的第一次接触(不算pre,跟这个相比Pre跟玩似的),本单元作业从被干烂的hw1开始,到最后基本没有被发现bug,心里还是比较爽的。

和面向过程相比,OOP的架构设计显得尤为重要,当年看到题目就开始写代码的日子已经一去不复返咯,我们需要使劲琢磨数据的组织方式,处理方式,需要考虑各种需要考虑的复杂问题。

希望能够活过今后的电梯月!

posted @ 2022-03-23 11:50  TrickEye  阅读(131)  评论(2编辑  收藏  举报