BUAA_OO_Unit1 后缀表达式完成表达式解析

王增煦-22373137 2024-03-23 17:11:21

BUAA_OO_Unit1 后缀表达式完成表达式解析

前言

本次面向对象设计与构造的第一单元内容是对表达式的化简,包括加减乘法运算、幂函数、指数函数、自定义函数及求导运算,最终实现表达式括号展开,以及化简输出。
在指导书中我了解到递归下降这一核心概念,正则表达式法,对于容器等的使用,以及词法分析文法分析等重要概念,可以通过Lexer类和Pasrer类将表达式解析为表达式因子这一基本层次结构,最终完成表达式的化简工作。

HW1

一、类图

img

二、架构设计

通过预习指导书,我了解到递归下降这一存储表达式的方法,通过Pasrer类中parseExprparseTermparsePowerparseFactor方法递归调用完成表达式的层层解析,虽然正则表达式也能够完成第一次作业,但是其复杂阅读性差,且不能够很好的完成后续作业的迭代任务,所以本单元的作业核心还是采用递归下降这一方法,在后续计算的工作中,我借鉴了 Training1 的思路——后缀表达式法,通过栈完成计算。总体来说,在三次作业的迭代过程中,我均采用了递归下降加后缀表达式的主体架构。

下面是笔者的层次结构及所建的类:

存储设计:

  • 与training中不同的是此次作业中加入了幂因子,开始我考虑将其归到Factor类,但是其底数部分可能是Expr类,还会用到递归,索性建立新的Power类,位于Term类和Factor类之间,其包括底数(Factor)和指数(Num)。
  • 整体层次结构采用Expr - Term - Power - Factor这四个基本类的结构,对于Num(常数因子)、Var(变量因子)、Expr(表达式因子)则很容易想到通过接口interface Factor实现。(具体结构如下图所示)

img

  • Expr(表达式类):存放Term类和运算符Op,并重写toString方法打印后缀表达式(term1 term2 op1)。
  • Term(项类):存放Power类,并重写toString方法打印后缀表达式(power1 power2 *)。
  • Power(幂因子类):存放底数(Factor)和指数(Num),并重写toString方法打印后缀表达式(factor num ^)。
  • Factor(因子类):因子类接口,包括Expr、Num、Var。
    • Num(常数因子)
    • Var(变量因子):在HW1中只有x

词法分析类(Lexer)

  • 完成表达式的预处理:
    • 删去空白项。
    • 将连续多个+ -换成一个,^后的+删去。
    • 若字符串首个字符为运算符,则在首位添加0.
  • 扫描并返回下一个符号(因子、运算符、括号)

语法分析类(Parser)

  • parseExpr():对于当前表达式,调用parseTerm解析其中的项,将解析到的Term、Op存入new Expr
  • parseTerm():对于当前项,调用parsePower解析幂,并存入Power。
  • parsePower():对于当前幂,调用parseFactor解析幂,并存入底数Factor和幂Num。
  • parseFactor():
    • 遇到(则调用parseExpr解析后面遇到的表达式。
    • 遇到常数因子Num或变量因子Var则直接返回其值。

运算类(Operator)

  • 通过栈类Stack存放基本项,及运算方法。

    运算设计

  • 通过将各个类中的toString方法重写,把我们的解析语法树输出后缀表达式。像数据结构中所学的栈计算后缀表达式那样,通过Stack栈类存储基本项a*x^n,出栈调用运算方法,再将结果入栈最后输出。
  • 因为本次作业中的变量因子只有x,我们完全可以用Hashmap<Integer,BigInterger>这一容器完美的解决计算问题,将指数作为key、系数作为value,在将来的合并同类项中,只需比较key检索合并value系数。(建议在第一次作业中就将指数设置为BigInteger类型,避免第二次作业因指数过大而爆掉)。遇到则表达式入栈,遇到运算符则出栈,并调用运算方法计算再将结果入栈,最终输出栈顶表达式。

Bug分析

  • 在第一次作业中我出现的bug出现在了对于-号的处理。阅读指导书我们知道在第一个因子之前,可以带一个正号或者负号,这就会导致多个运算符连续出现的情况,例如x*-x2*(-x+1)。起初我的解决方法是在进行词法分析时,连带因子前面的符号一起解析,即存入正负数和正负x,看似解决了问题,但是我忽略了-(这种情况,导致强测出错并在互测中被多次hack。最终我选择在表达式字符串预处理阶段将连续符号进行处理(例如str = str.replaceAll("-(", "-1*(")),达到了一劳永逸的成效。在后续的迭代过程中,只需对新加入的符号进行类似的预处理就能避免该类bug。
  • 他人的bug主要集中在多符号的处理(例如无法消除多个连续加号)。在互测中,我发现别的同学利用正则表达式匹配去掉多余符号,但是采用这种方法十分冗长且可能漏掉特例导致bug出现,需要注意。

HW2

一、类图

img

二、架构设计

在第二次作业中引入了指数函数因子、自定义函数因子以及多层嵌套括号。
其基本项变为a*x^b*exp(Expr)

  • 首先对于多层嵌套括号,由于第一次作业采用了递归下降法,能够解析多层括号,所以我们的架构天然的解决了多层嵌套括号的问题。

  • 接下来是指数函数因子,我定义了一个Exponent类存储指数函数因子,该类属性仅包含Factor因子类。

    public class Exponent implements Factor {
      private ArrayList<Factor> factors;
    }
    

    我的思路是重写toString,将exp作为一元运算符输出到后缀表达式中(factor1 exp)在后续的计算中进行处理。

  • 最后是自定义函数的处理,通过定义Function类存入定义的表达式,正则表达式法解析输入的自定义函数字符串

    public class Function {
    private final HashMap<String,ArrayList<String>> functions;
    ...
    Pattern myPattern = Pattern.compile("([fgh])\\(([uvw,]+)\\)=(.+)");
    }
    
    • key中存储函数名,ArrayList存储形参和函数表达式。
    • 对于函数的替换,我的方法是在语法解析的递归调用中,读到函数名则通过parseExpr捕获实参,再调用Function类中的preprocessor方法替换表达式定义式中的形参,并返回替换表达式(Expr类),继续进行语法解析。
    • 这里需要注意的是,实参代替形参的顺序
      例如,我们定义函数 f(y,x)=x+y,那我们输入f(x,x^2)的结果本应为 x+x^2,但是若先代入y后代如x,计算过程就会是这样的f(x,x^2) -> x+x -> 2*x^2。因此为了解决这一问题,我们干脆将形参x,y,z全部用u,v,w代替(注意exp中的 x )。

      运算设计

  • 本次引用 Poly 这一算子类,该类包括ArrayList存储 Letter类,而其中的Letter类用于存储每一个基本项的系数(BigInteger)、幂(BigInteger)、exp指数(Poly)。Letter为一个基本项a*x^b*exp(Poly),而Poly即为多个基本项的和,注意由于exp指数还可能是表达式的嵌套,所以将指数部分定义为Poly类,即Poly类和Letter类的嵌套循环。

    public class Poly implements Cloneable {
        private final ArrayList<Letter> letters;
        ...
    }
    public class Letter implements Cloneable {
        private BigInteger coe; //系数
        private BigInteger exp; //幂
        private Poly poly;      //指数
        ...
    }
    
  • 运算方法包括

    • addPoly:其中的难点在于如何合并同类项,笔者采用了重写Poly类和Letter类中的equals方法,递归判断两项是否相等。
      public boolean equals(Poly poly) {
          if (this.letters.size() != poly.getLetters().size()) {
              return false;
          }
          for (Letter i : this.letters) {
              ...
          }
          return true;
      }
      
      public boolean equals(Letter letter) {
          if (!this.coe.equals(letter.getCoe())) {
              return false;
          } else if (this.coe.equals(new BigInteger("0"))) { //递归终止条件
              return true;
          } else if (!this.exp.equals(letter.getExp())) {
              return false;
          } else {
              return this.poly.equals(letter.getPoly());
          }
      }
      
      for (Letter i : clonePoly1.getLetters()) {
              for (Letter j : clonePoly2.getLetters()) {
                  if (j.getExp().equals(i.getExp()) && j.getPoly().equals(i.getPoly())) {
                      ...
                  }
              }
          }
      
    • subPoly\mulPoly\powPoly:调用addPoly方法,注意mulPoly的实现。
    • expPoly:直接将栈顶Poly置指数位置上。

Bug分析

至此我们解决了HW2的全部难点,但是在我测试样例的时候发现了一个奇怪的bug,就是在powPoly的运算中,我通过调用mulPoly方法进行计算,本应与乘法计算结果相同才对,但是却差距甚大。起初我猜想可能是乘法因子在计算过程中参与计算使其值发生改变,但很遗憾并不是这个问题。我最终发现其罪魁祸首便是深浅克隆问题,在java中对象的赋值是引用赋值,即浅克隆,克隆的是对象的地址,所以在powPoly计算的过程中引用了addPoly方法改变了它的值,所以在乘方的过程中,因子一直在改变导致了结果的错误。

  • 解决方法就是深克隆,即重写clone()方法。
    Poly clonePoly1 = new Poly();
    Poly clonePoly2 = new Poly();
    // 深克隆 poly1
    for (Letter letter : poly1.getLetters()) {
        clonePoly1.addLetter((Letter) letter.clone());
    }
    // 深克隆 poly2
    for (Letter letter : poly2.getLetters()) {
        clonePoly2.addLetter((Letter) letter.clone());
    }
    ...
    @Override
    public Object clone() throws CloneNotSupportedException {
        return (Letter) super.clone();
    }
    @Override
    public Object clone() throws CloneNotSupportedException {
        return (Poly) super.clone();
    }

HW3

一、类图

img

二、架构设计

在第三次作业中新增了允许调用已定义的自定义函数和求导算子,我们的架构天然满足新增的自定义函数要求,所以本次作业只需着眼于求导算子。
由于架构基本成型,本次作业的代码修改量很小,仅增加了Derivation求导类。

  • 首先定义了DeExpr类,存储需要求导的表达式,重写toString方法,将dx作为一元运算符输出到后缀表达式中。
  • 定义了Derivation求导类,内含对表达式求导的方法。当读到dx运算符时,对栈顶表达式poly进行求导运算。

度量分析

img

img

img

img

  • Lexer(词法解析类)\Operate(计算类)\Poly(算子类)这三个类的复杂度较高,可以看出主要集中在解析过程和计算过程中,以及toString方法存在很高的复杂度。值得注意的是,作业中出现的bug多集中在这些复杂度较高的类中。

心得体会

  • 通过第一单元面向对象课程的学习,我学到了面向对象的思维,封装与继承,容器迭代器等的使用,以及如何实现“高内聚低耦合”,除了学到的专业技能,我认为更重要的还是在设计的过程中发现问题并自己动手解决问题的过程,这种不断探索、不断完善的致学态度才是伴随我们求学路上最宝贵的财富。
  • 回顾OO第一单元三次作业,一路磕磕绊绊,最终顺利完成实感庆幸。由于笔者在上学期没有学习pre课程,但好在假期学了一下java语法,在开始第一次作业之前恶补了容器、继承、接口等面向对象知识,才侥幸拿下第一次作业,在后续的迭代中,通过不断学习新知识,秉持着面向对象“高内聚低耦合”的特点一点点顺利地完成了任务。从第一周一度怀疑第一次作业就是不能完成的挑战,每天梦中思考采用怎样的架构,如何解析计算;到第二次作业欣喜完成代码后连找两天bug的痛苦历程;再到最终第三次作业中一晚上搞定HW3,且在强测与互测中未出现bug。过程虽然艰辛,但是在不断探索与克服困难的过程中,自己也收获了肉眼可见的成长。
  • 在学习学长博客的过程中看到了一句话对当时的我鼓励很大,也感慨颇深,而编写代码的过程又好像自己是上帝在创造一个新事物一样,看到结果正确的瞬间会收获快乐。
...全文
126 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧