BUAA 面向对象设计与构造 Unit 1 表达式化简

钟昊翔-21231025 学生 2024-03-21 10:59:41

BUAA 面向对象设计与构造 Unit 1 表达式化简

第一次作业

题目描述

读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号后的表达式。

整体架构

  • 语法解析

    针对输入的解析,我使用的是递归下降法,解析完成后再进行计算。涉及到的类有:

    • Lexer:将输入的表达式分解为一个个token

      token类型有:数(包含一个或多个数字)、运算符(+,-,*,^)、括号和变量(x)

      在获取token的时候会自动跳过所有的空白字符(空格和\t)

    • Parser:按照形式化表述的要求解析token流

      形式化表述如下:

      • 表达式 →→ 空白项 [加减 空白项] 项 空白项 | 表达式 加减 空白项 项 空白项
      • 项 →→ [加减 空白项] 因子 | 项 空白项 '*' 空白项 因子
      • 因子 →→ 变量因子 | 常数因子 | 表达式因子
      • 变量因子 →→ 幂函数
      • 常数因子 →→ 带符号的整数
      • 表达式因子 →→ '(' 表达式 ')' [空白项 指数]
      • 幂函数 →→ 'x' [空白项 指数]
      • 指数 →→ '^' 空白项 ['+'] 允许前导零的整数 (注:指数一定不是负数)
      • 带符号的整数 →→ [加减] 允许前导零的整数
      • 允许前导零的整数 →→ ('0'|'1'|'2'|…|'9'){'0'|'1'|'2'|…|'9'}
      • 空白项 →→ {空白字符}
      • 空白字符 →→ (空格) | \t
      • 加减 →→ '+' | '-'
  • 类结构

    • Constant:常数

      属性:

      BigInteger constant // 存储常数的数值

    • PowerFunction:幂函数

      x^index

      属性:

      String variable // 存储变量的名称(虽然这次作业只有x)

      BigInteger index // 存储幂函数的指数

    • Term:(不含表达式因子的)项

      任何不含表达式因子的项都可以化简为coefficient * x^index,因此只需要存储系数和指数即可,本次作业因为对后续可能加入的不同变量名作出了预留,所以采用了Arraylist存储,实际上只需要存一个指数即可。

      属性:

      BigInteger coefficient // 系数

      ArrayList<PowerFunction> powerFunctions // 幂函数数组

    • Expression:表达式

      任何的表达式都可以表达为Term + Term + ... + Term,因此只需要存储Term数组

      属性:

      ArrayList<Term> terms // 项数组

    • Factor:因子接口,常数、幂函数和表达式均实现Factor接口

  • 类图如下:

img

  • 计算过程:

    • 初始化:

      new Expression()初始化为0,即空数组

      new Term()初始化为1*x^0,即1

    • 计算

      1. Term * Constant

        直接将ConstantTerm.coefficient相乘,并返回新的Term

      2. Term * PowerFunction

        PowerFunction.index + Term.powerFuncion.index相加,作为新的Term.powerFunction.index

      3. Term1 * Term2

        对系数和幂函数分别执行1、2即可

      4. Expression + Term

        如果Expression中有Term的同类项,则在原有Term的基础上系数相加

        反之Expression.terms.add(Term)直接加入ExpressionTerm数组

      5. Expression * Term

        Expression中每一个Term进行上文的乘法操作即可

      6. Expression1 * Expression2

        Expression1Expression2中的所有项,分别一一执行3即可

      7. Expression^index

        重复调用index次6即可(如果index == 0则直接返回1

      8. Expression1 + Expression2

        Expression2中的每一项与Expression1执行4即可

      注:计算方法均为静态方法,返回Expression的均为Expression类的静态方法,返回Term的均为Term类的静态方法,这些方法在计算的时候返回的结果均为深拷贝的new对象,避免了可能出现错误修改引用问题。

  • 输出过程:

    输出的时候对每一个类均重写了toString方法,在输出的时候使用StringBuilder将个项连接

    • Constant

      直接返回数字对应字符串

    • PowerFunction

      如果指数为0,返回1

      如果指数为1,返回x

      反之返回x^index

    • Term

      如果系数为0,返回0

      如果系数为1,返回PowerFunction.toString()

      如果幂函数指数为0,返回coefficient

      反之,返回coefficient*x^index

    • Expression

      如果没有项,返回0

      反之返回带符号的项的拼接,(-) Term +/- Term +/- ... +/- Term

分析

  • 优化

    优化主要在计算过程中的合并同类项,和输出过程中对..*1这种类型的化简

    还可以做的优化是在输出的时候让第一项为正项,这样可以少一个负号

  • 复杂度分析

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

    MethodCogCev(G)iv(G)v(G)
    Parser.termParser()1621111
    Term.likeTerms(Term, Term)16959
    ...............
    Total12183113139
    Average2.471.692.312.84
    ClassOCavgOCmaxWMC
    Constant115
    Expression2.5625
    Lexer2.2559
    Main111
    Parser3.881131
    PowerFunction2.31023
    Term3.27936

    复杂度比较高的方法分别为Parser.termParser()(解析Term)和Term.likeTerms(Term, Term)判断两个项是否为同类项。在第一次作业的时候,由于常数因子、幂函数因子和表达式因子都只是继承了因子接口,但是没有共同的方法,所以在Parser.termParser()中,针对不同的因子种类手动调用了不同的计算方法,导致复杂度很高。

    Factor factor = factorParser();
    if (factor instanceof Constant) {
        term = Term.mulFactor(term, (Constant) factor);
    } else if (factor instanceof PowerFunction) {
        term = Term.mulFactor(term, (PowerFunction) factor);
    } else {
        expr = new Expression((Expression) factor);
    }
    

    判断同类项的过程由于为了支持多变量写了很多多余的操作,所以复杂度也很高,但是其实可以不加。

  • 发现bug策略

    有的同学并没有在处理表达式的时候进行同类项合并,这样就会导致形如(x-x+x-x+x...+x-x+1)^8这样的数据由于长度过长导致爆栈或者超时。也有项或者表达式不输出0的情况。

第二次作业

题目描述

在上次作业的基础上,读入一系列自定义函数的定义以及一个包含幂函数、指数函数、自定义函数调用的表达式,输出恒等变形展开所有括号后的表达式。

整体架构

  • 语法解析

    和第一次作业一样,使用递归下降法,稍作修改,并且加入了用于解析函数的FunctionParser

    • Lexer:加入对exp的检测

    • Parser:加入对exp的检测

      指数函数 →→ 'exp' 空白项 '(' 空白项 因子 空白项 ')' [空白项 指数]

    • FunctionParser:对函数的翻译,与Parser不同的是他的每一步返回均为字符串,只用于函数形参的带入

      在带入的时候,把函数表达式中的x,y,z替换为对应的实参

      注:为了防止把exp中的x也替换,在处理的时候先把exp换成eee了

类结构

  • 整体结构没有太大变化,在上次的基础上增加了:

    • ExponentialFunction

      幂函数,exp(index),其中index为一个Expression类的表达式

    • Term

      由于加入了幂函数,因此任何不含表达式因子的项可以写成coefficient * powerFunction * exponentialFunction,因此只在上次的基础上加入了幂函数。

  • 计算

    本次作业针对计算做了比较大的修改,核心目的是为了将计算方法分隔开,更好的封装

    所有的计算方法均为静态方法,并且返回新的结果对象

    // Constant
    Constant multiply(Constant c1, Constant c2);
    Constang add(Constant c1, Constant c2);
    // PowerFunction
    PowerFunction multiply(PowerFunction p1, PowerFunction p2);
    // ExponentialFunction
    ExponentialFunction multiply(ExponentialFunction e1, ExponentialFunction e2);
    // Term
    Term multiply(Term t1, Term t2);
    Term multiply(Term t, Constant c);
    Term multiply(Term t, PowerFunction p);
    Term multiply(Term t, ExponentialFunction e);
    Term add(Term t1, Term t2); // 同类项才能相加
    // Expression
    Expression add(Expression e, Term t);
    Expression add(Expression e1, Expression e2); // 依次调用表达式+项即可
    Expression multiply(Expression e, Term t);
    Expression multiply(Expression e1, Expression e2); // 依次调用表达式*项即可
    

    为了方便计算,所有的类都有一个方法将其转化为表达式,即toExpression()方法。

  • 输出过程

    与上次基本相同,添加了指数函数的输出,格式为:exp(index),其中index为表达式类型

  • 类图

img

分析

  • 优化

    本次可以优化的地方在上一次的基础上,加入了exp(index)的优化,主要就是提取指数内的公因数。

    exp(999999+999999*x)可以优化为exp(1+x)^999999

  • 复杂度分析(只列举较高的几个方法)

    MethodCogCev(G)iv(G)v(G)
    Expression.add(Expression, Term)10166
    Parser.factorParser()12677
    ...............
    Total138135193218
    Average1.351.321.892.14
    ClassOCavgOCmaxWMC
    Constant1110
    CustomFunction1.839
    ExponentialFunction1.1229
    Expression2.5630
    FunctionParser2.6626
    Lexer2.4712
    Main333
    Parser3.56732
    PowerFunction1.1229
    Term2732

    可以看到,这次的作业复杂度相对上一次有了显著降低,因为大部分的计算方法都被封装成了单独的方法,并且不同类之间的交互也减少到了最低。

  • 发现bug策略

    由于本次指数函数的形式化表述为:

    exp(因子)[指数]

    因子中幂函数的形式化表达是:

    x[指数]

    导致exp(-x)这样的表达式不符合形式化表达,因为-x不是因子,这点许多出错的。

第三次作业

题目描述

读入一系列自定义函数的定义以及一个包含幂函数、指数函数、自定义函数调用、求导算子的表达式,输出恒等变形展开所有括号后的表达式。

整体架构

  • 语法解析

    和之前的做法没有任何区别:

    • Lexer:加入dx的解析

    • Parser:加入dx的解析

      形式化表述如下:

      • 求导因子 →→ 求导算子 空白项 '(' 空白项 求导因子 空白项 ')' | 求导算子 空白项 '(' 空白项 表达式 空白项 ')'
      • 求导算子 →→ 'dx'
    • FunctionParser:加入对dx的解析,并且由于本次函数定义支持递归声明,因此在处理的时候采用了执行3次FunctionParser,这样无论三个函数的递归顺序如何,均能完成带入。

      在带入的时候,把函数表达式中的x,y,z替换为对应的实参

      注:为了防止把dx中的x也替换,在处理的时候先把dx换成dd了

类结构

整体类结构和上次作业没有任何变化,只有每一个基础类加入了一个方法derive(),该方法返回统一为表达式。

// Constant
public Expression derive() { 
    return Expression.ZERO; 
}
// PowerFunction
public Expression derive() {
    PowerFunction p = new PowerFunction(variable, index.subtract(BigInteger.ONE));
    Constant c = new Constant(index);
    return Expression.multiply(c.toExpression(), p.toExpression());
}
// ExponentialFunction 使用链式法则
public Expression derive() {
    return Expression.multiply(this.toExpression(), index.derive());
}
// Term 使用乘法原理
public Expression derive() {
    return Expression.multiply(coefficient.toExpression(),
        Expression.add(
            Expression.multiply(powerFunction.toExpression(), exponentialFunction.derive()),
            Expression.multiply(powerFunction.derive(), exponentialFunction.toExpression())
        )
    );
}
// Expression
public Expression derive() {
    Expression res = new Expression();
    for (Term term: terms) {
        res = Expression.add(res, term.derive());
    }
    return res;
}
  • 类图

img

分析

  • 优化

    本次作业没有什么可以在上次作业的基础上新加入的优化,针对长度的优化和表达式计算速度的优化都和上次的一样。

  • 复杂度分析(只列举较高的几个方法)

    MethodCogCev(G)iv(G)v(G)
    Expression.add(Expression, Term)10166
    Parser.factorParser()13788
    FunctionParser.parseFactor()9199
    ...............
    Total145132194219
    Average1.481.351.982.23
    ClassOCavgOCmaxWMC
    Constant1111
    CustomFunction1.839
    ExponentialFunction1.11210
    Expression2.46632
    FunctionParser2.64729
    Lexer2.6813
    Main444
    Parser3.5835
    PowerFunction1.11210
    Term1.94733

    可以看到,基本上复杂度和上次作业没有太大区别,大部分方法复杂度都比较低,高复杂度的方法由于<因子>的形式化表达本就很复杂,种类繁多,因此复杂度无法再做优化。

  • 发现bug策略

    基本上大部分的bug都在上次的强测中被检测出,因此这次主要还是在测试求导算子上,比如dx(dx(exp(0)))这种。

心得体会

在完成第一次作业的时候,由于考虑了太多的扩展性,导致完成了很多不必要的工作,并且后续并没有在对应方向上拓展的需求,导致在第二次作业的时候为了简化结构把许多内容都删掉了,浪费了很多时间。以后在留扩展性的时候可以不先加入数据结构,而是改为加入新的类,然后重写判断方法,这样可以避免一开始就用HashMap导致代码复杂。

在设计类的时候,要最大限度的减少类之间的耦合,计算方法要返回新对象,不然可能会出现不可预料的奇怪错误。这点在调试的时候花费了许多时间,因此后几次代码都是采用了不可变类,即各因子在生成的时候即确定,之后内容不再改变,计算的时候均在复制的因子对象中进行修改。

未来方向

我认为这几次作业的主要结构都是递归关系,但是对继承、接口相关并没有太多涉及,各因子继承自<<因子>>接口这个方法其实可有可无,以至于后两次作业采用toExpression()方法反而更简单。本次作业最适合用接口的应该是“可导”这个性质,只要实现了求导方法的因子都可导。

可以加入除法运算,输出的时候输出分子分母即可。

...全文
88 1 打赏 收藏 转发到动态 举报
写回复
用AI写文章
1 条回复
切换为时间正序
请发表友善的回复…
发表回复
魏东霖-22371041 学生 2024-10-31
  • 打赏
  • 举报
回复

301

社区成员

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

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