BUAA_2024_OO Unit1 表达式解析和化简

刘思涵-22371386 2024-03-22 15:22:23

前言

在OO课程之前,我并没有参加OOpre课程,因此这是我第一次接触OO编程,其中许多地方架构混乱,解耦合不完善,因此本篇博客仅作为交流学习,希望大家能在各自的博客中获得灵感,共同进步。

第一次作业——对表达式建模并展开

实验思路

第一次作业要为第一单元打好基础,在设计架构时尽可能考虑到之后会出现的新需求,因此保证第一次作业架构的简洁性和拓展性是相对重要的。我在第一次作业中,考虑到了可能会变和可能不变两种类型。

首先是可能不变的部分:

首先是表达式的基本结构,无论其中内容发生怎样的改变,无论是否会增加新的函数和变量,表达式的层次结构是不会变的。

表达式总可以表达为多个不同项的和,而项也可以表达为多个不同因子的积,这是指导书中提示我们的内容,也是一个表达式的基本属性。

当做到第三次作业时,再回看第一次作业的架构,即使后来出现了指数和求导两种因子,但总的来说,既可以解释为一种特殊的因子,其中包含表达式,也可以解释为一个新的表达式,而具有不同的属性。在具体实现中,我倾向于将指数看作因子,而导数看作表达式。

综合以上,表达式的结构是固定的,因此我们在设计数据结构时的架构也是固定的,即以树结构形成表达式树。

树可以按照形式化表示的内容来设计,其中主要分为表达式,项,因子三个层次,在表达式因子中还可以再保存下一层的表达式,因此形成一个无限延伸的树状结构,层次化向下伸展。

具体结构如图所示:

img

其次是可变部分:

在指导书中有如下描述:

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

  1. 在表达式中仅包含加,减,乘,乘方

可能要出现其他的计算方式,如何面对新增的计算方式对结构的影响极其重要?

  1. 括号的深度至多为1层

可能会出现多层括号嵌套,如何理解括号,如何将括号的深度拓展到无限层?

由自变量 x,指数符号 ^ 和指数组成,指数为一个非负带符号整数,如:x ^ +2,x ^ 02,x ^ 2

  1. 自变量仅有x

可能出现多重不同的变量,如何处理不同的变量,以及如果出现多字母变量?

  1. 指数为一个非负符号整数

可能出现负指数?

用一对小括号包裹起来的表达式,可以带指数,且指数为一个非负带符号整数

  1. 指数非负

依然是这个问题,但如果出现负数能否化简

  1. 整数

包括上面的数均为整数,之后是否会出现小数,包括数字和指数部分,此时如何表示小数,能否用分数形式表示?

整数的范围并不一定在int或long范围内

  1. 不一定在int或long范围内

已明显表明,因此要涉及到其他的数字保存方法


综合以上内容,我们已经对于未来可能出现的情况作了一个简单的预期,并为保证架构的拓展性,因此要考虑这些未来可能的发展,尽可能完善架构。

除此之外,还有性能判定,因此程序中不仅要考虑输出答案的正确性,还要尽可能考虑程序的性能,如何使程序能够化简?

架构设计

首先放上大致的类图:

img

到了具体实现的部分后,就发现程序实际上可以分为几个大块,以程序的大致运行流程为序:

  • 读入文本
  • 解析表达式,建树
  • 合并表达式,化简
  • 输出表达式
其中第二步和第三步,都有着递归的特性:
  • 在解析表达式建树时,我们可以根据表达式,项,因子的层次,直接的将一个表达式通过加减号分拆成为一个个单独的项
  • 这些项又可以根据乘号拆分成为一个个具体的因子,实际过程如下:

img

首先将表达式根据加减号拆分成为一个个独立的项

img

再次将表达式根据乘号拆分为一个个独立的因子

img

根据因子中的数据类型不同,将其分为不同的因子保存,分别是:

  • 数字因子(NumFactor)
  • 变量因子(VarFactor)
  • 表达式因子(ExprFactor)

这些因子都具有相同的特征,例如正负,指数,因此可以将这些属性作为接口的属性,规范因子类的行为

除此之外,在表达式因子中,在可以解释为一个新的表达式,而将外部的符号和指数作为因子的特性,不考虑到其中

再一切表达式都被解析完毕之后,应当可以建立一个完整的表达式树,其中由表达式和项以下,管理着许多包含特定数值的因子

那么既然建树已经完成,下一步就是化简了:

化简和建树都带有递归的特增,具体来说细节的话,建树是由一个完整地树根,一步步开枝散叶,建立出因子作为叶节点

而化简则是由外围的因子作为起点,一步步回到最初的那一棵最简表达式树,那么这个过程显而易见,可以参考DFS作为算法设计

项化简的过程大致如下:

  • 保证项中的每一个因子都是最简形式
  • 将因子之间能合并的均合并
  • 不能合并的因子并列保留
  • 因为已经保证了每一个因子都是最简,因此表达式因子也是最简,而且应不存在指数
  • 将表达式和剩余的因子相乘,得到表达式
  • 将表达式返回到上一层

表达式的过程:

  • 首先保证每一个项都是最简形式,此时每一个项都会返回一个表达式
  • 将这些表达式相加,并将相同的部分合并,进行加法合并(此时根据固定顺序的项的文本形式,匹配相同的项)
  • 将相加后的表达式返回

表达式因子的指数:

  • 如果因子为0,则保存一个只有1的表达式
  • 如果大于1,进行累乘并化简(可以考虑对数次累乘)

综上所述,可以保证在每一次处理时,当前层次下的内容都是最简形式,这样的化简思路减轻了我们设计程序时的复杂度

程序分析

首先给出由Metrics分析得到的复杂度结果,由于第一次实现面向对象思路的设计,因此在程序中有许多耦合度高的部分,但实际根据前面所设计的运行逻辑,这部分耦合度是可以降低的

MethodCogCev(G)iv(G)v(G)
Add.and(Expr, Expr)1122
Debug.debug(Expr)1122
Debug.debug(Factor)12177
Debug.debug(Term)1122
Debug.repeat(String, int)1122
Expr.Expr()0111
Expr.addTerm(Term)0111
Expr.getTerms()0111
Expr.setTerms(HashSet)0111
ExprFactor.ExprFactor(Expr, int, boolean)2133
ExprFactor.getExpr()0111
ExprFactor.isNegative()0111
ExprFactor.setNegative(boolean)0111
ExprParser.parse(String)1122
ExprParser.separate(String, int)1511011
ExprReducer.reduce(Expr)0111
ExprReducer.reduce(Expr, Boolean)14199
FactorParser.parse(String)1841013
FactorParser.separate(String, int)12167
MainClass.main(String[])4133
MainClass.testing(String)0111
Mult.clone(Expr, Expr)8166
Mult.parse(Expr, BigInteger)8455
Mult.parse(Expr, Expr)6144
Mult.parse(Expr, Factor)3144
Mult.parse(Expr, String, int)8455
NumFactor.NumFactor(BigInteger, int, boolean)0111
NumFactor.getIndex()0111
NumFactor.getValue()0111
NumFactor.isNegative()0111
NumFactor.setIndex(int)0111
NumFactor.setNegative(boolean)0111
NumFactor.setValue(BigInteger)0111
Output.outPut(Expr)15668
Term.Term()0111
Term.addFactor(Factor)0111
Term.addNum(BigInteger)3133
Term.getFactors()0111
Term.getNegative()13177
Term.getNum()6144
Term.getVar()9656
Term.newFactors(HashSet)0111
Term.toString()8155
TermParser.parse(String)1122
TermParser.separate(String, int)12167
TermReducer.combine(Expr)14177
TermReducer.reduce(Term)201811
Testing.calculate(String)0111
Testing.generate()0111
Testing.test()6434
VarFactor.VarFactor(String, int, boolean)0111
VarFactor.getIndex()0111
VarFactor.getVar()0111
VarFactor.isNegative()0111
VarFactor.setIndex(int)0111
VarFactor.setNegative(boolean)0111

可以看出程序的复杂度主要集中在解析和化简部分,而其中复杂度最主要是因为此部分代码没有使用面向对象的思路,仍为单一方法中对容器中内容直接进行访问,因此在后续的修改和迭代开发中也出现了难以维护的情况。经过这一单元的开发,我深刻认识到模块间解耦合对程序维护和迭代的重要意义,当面对数百行互相关联的代码时,体现出的无力感着实令人焦躁

部分模块的设计
  1. 去除输入中的若干空白字符
            expression = expression.replaceAll("[ \t]", ""); // 去掉所有空格和制表符

运用简单的正则表达式即可实现

  1. 分离各个项,因子
        Expr expr = new Expr();
        int i = 0;
        while (i < expression.length()) { // 遍历表达式
            int j = separate(expression, i); // 分离项
            String sub = expression.substring(i, j); // 设项为子串
            Term term = TermParser.parse(sub); // 解析项
            expr.addTerm(term); // 添加项
            i = j;
        }
        expr = ExprReducer.reduce(expr); // 简化表达式
        return expr;

采用递归的思路,借助一个项结尾的特征设计分离函数(项的结尾一定是字母或括号链接正负号),将子项作为新的字符串进行解析

在获得表达式时,一定会调用化简,因此解析得到的所有表达式一定是最简形式(运算得到的表达式不一定最简)

        Term term = new Term();
        int i = 0;
        while (i < sub.length()) {
            int j = separate(sub, i); // 分离因子
            String subsub = sub.substring(i, j);
            Factor factor = FactorParser.parse(subsub); // 解析因子
            term.addFactor(factor);
            i = j + 1;
        }
        return term;

因子同理,因子之间的分隔符为*,注意跳过括号中的内容

  1. 解释因子内容
        /**
         *  前面分离因子的正负号和指数,指数分隔符为^
         */
        if (subsub.charAt(i) == '(') { // 表达式因子
            Expr expr = ExprParser.parse(subsub.substring(i + 1, tj - 1));
            return new ExprFactor(expr, index, isNegative);
        } else if (Character.isDigit(subsub.charAt(i))) {
            return new NumFactor(new BigInteger(subsub.substring(i, tj)), index, isNegative);
        } else if (Character.isLetter(subsub.charAt(i))) {
            return new VarFactor(subsub.substring(i, tj), index, isNegative);
        } else {
            return null;
        }

可见,在获得新的表达式因子之前,会将表达式的内容化简,因此得到一个完全最简的表达式因子,满足程序设计时的假设

  1. 化简项
        for (Factor factor : term.getFactors()) {
            if (factor instanceof VarFactor) {
                VarFactor varFactor = (VarFactor) factor;
                String var = varFactor.getVar();
                if (varMap.containsKey(var)) {
                    varMap.put(var, varMap.get(var) + varFactor.getIndex());
                } else {
                    varMap.put(var, varFactor.getIndex());
                }
                if (varFactor.isNegative()) {
                    isNegative = !isNegative;
                }
            } else if (factor instanceof NumFactor) {
                NumFactor numFactor = (NumFactor) factor;
                integer = integer.multiply(((NumFactor) factor).getValue());
                if (numFactor.isNegative()) {
                    isNegative = !isNegative;
                }
            } else if (factor instanceof ExprFactor) {
                ExprFactor exprFactor = (ExprFactor) factor;
                Expr expr1 = ExprReducer.reduce(exprFactor.getExpr());
                list.add(expr1);
                if (exprFactor.isNegative()) {
                    isNegative = !isNegative;
                }
            }
        }

在化简项时,为了以后多变量考虑,此时现将其中的变量名提取出来,并建立一个变量名和指数相结合的HashMap,以此存放表达式中的内容,如果有相同变量名的因子,则将指数相加

如果有数字,则将数字乘在一起,留作整个项的参数

如果有表达式(括号),则将括号中的表达式先存进队列,等待后续处理

        Term newTerm = new Term();
        for (String var : varMap.keySet()) { // 缩减变量
            VarFactor varFactor = new VarFactor(var, varMap.get(var), false);
            newTerm.addFactor(varFactor);
        }
        NumFactor numFactor = new NumFactor(integer, 1, isNegative); // 整合数字
        newTerm.addFactor(numFactor);
        Expr newExpr = new Expr();
        newExpr.addTerm(newTerm);
        for (Expr exprF : list) {
            newExpr = Mult.parse(newExpr, exprF);
        }
        newExpr = combine(newExpr); // 化简数字
        // TODO
        return newExpr;

将化简后的项提取出来,并分别于队列中等待展开的表达式相乘,得到的新表达式再进行合并,最终返回一个表达式

Bug分析

本次作业bug共一处:

if (index == 0) { // 次数为0则清零
    Term t = new Term();
    t.addFactor(new NumFactor(BigInteger.ONE, 1, false));
    te.addTerm(t);
}

在表达式因子的指数为0时,本应该直接将表达式清零,并存储一个新的为一的表达式,但可惜在此处忘记先清楚表达式的内容了

互测Bug

为了检测同学的代码是否有bug,我设计了一个简单的测试点生成器,因为设计简单,所以不保证所有的数据点均符合互测要求,所以只用来检查同学代码的bug,而一旦发现bug后再构造一个简单的数据进行hack

这个生成器在第二次作业时进行了修改,以适配第二次作业,因此第一次没有留档,在这里一并于第二次作业部分做介绍

简单说一下发现的bug:

  1. 在变量前的符号被指数所计算
  2. 乘法运算时符号出错
  3. 解析因子时出错,无法处理连续出现的符号
  4. 进行高次方运算时丢失内容

优化

在本次作业中,首先考虑的是程序的拓展性,因此没有过多关心优化的问题,程序中涉及的优化仅一下内容:

  1. 删除不必要的符号和参数,例如正负1
  2. 假设表达式化作最简形式,减轻程序设计复杂度
  3. 使用HashMap和规范化变量格式进行化简,减少运算压力
  4. 给出每一次表达式都是最简形式的假设,减少程序设计的复杂度,同时省去大量不必要的化简消耗
  5. 在出现指数为0时直接清空(然而出了bug......)

第二次作业——增量内容,修复Bug

实验思路

本次作业要求增加了指数因子和自定义表达式,以及多层嵌套的括号

首先是关于不可变部分,整体表达式的结构(层次化结构)并没有发生改变,这也说明第一次作业时作的假设成真

除此之外,考虑第一次开发时预留的拓展内容,本次作业的几个新要求,也可以被化作第一次作业拓展性的一种变形

例如多层嵌套的括号,这是在第一次就已经被考虑到的,因此几乎不需要作修改

其次是指数因子,在实验中主要有两种思路:

  • 将指数作为因子,添加一种新的因子类型
  • 将指数作为表达式的特征,表示为一种特殊的表达式类型

但实际上指数具有几个特点:

  • 指数不可以被展开
  • 具有相同表达式的指数可以被合并

因此指数的特点是与变量相似的,但是指数内部含有一个新的表达式,因此最合适还是增加一个指数因子

但是因为不想大幅度修改其他部分的内容,因此我将指数作为一种特殊的幂函数来表示,复用了变量因子

再其次就是自定义函数,这部分是这次作业相对复杂的部分

但是因为我之前的表达式中已经支持了表达式的多变量,因此可以直接将自定义函数的具体内容,作为一个表达式解析

解析出的表达式,是一个由x, y, z组成的最简表达式,此时要考虑函数的几个问题

  • 如果出现f(y, x) = x + y 之类的情况,可能会重复替换
  • 如何判断表达式中形参的顺序,代表哪一个参数?
  • 如果出现表达式嵌套,应当怎样处理

而且表达式的处理方法也大致有几个方案

  • 直接进行字符串替换
  • 在解析表达式时再进行替换

为了尽可能不修改原有的表达式解析部分,我这里选择使用字符串替换的方法

同时为了处理以上几个问题,我首先将表达式中的参数替代为@0, @1, @2等不会重复的占位符,并将预处理好的函数存为函数名——函数的HashMap

架构设计

img

由图可见,第二次对架构的修改较少,大部分内容可以沿用第一次作业

关于整体流程部分,可以增加两个新内容:

  • 读入文本
  • 预处理自定义函数
  • 将函数结合实参进行字符串替换
  • 解析表达式,建树
  • 合并表达式,化简
  • 输出表达式

具体操作流程为:

  1. 将表达式拆解为函数名,形参列表,化简后的表达式
  2. 结合形参列表,分别按照顺序将形参替换为占位符,输出为字符串
  3. 得到预处理后的函数,具体内容为函数名和带有形参的表达式字符串
  4. 对输入的表达式进行搜索,寻找表达式的特征(字母后紧跟括号,但不是exp)
  5. 根据函数名寻找目标表达式,提取函数的实参,按照顺序分别对预处理的表达式进行替换
  6. 将替换后的字符串替换掉原有的函数符号
  7. 循环此过程,直到不再发现新的函数

至于指数的部分,我直接附在变量因子内,乘法合并时只需要将两个表达式相加即可,而加法合并时则保证按规律输出的表达式相同,则可以合并,具体合并逻辑和变量是相同的

重构

在本次作业中,我对第一次中的部分耦合度较高的部分进行了重构,主要内容包括:

  • 将独立出来的变量乘法,加法的部分集成到对象内部,作为对象的方法使用
  • 将输出字符串的内容修改到对象的toString方法中(结果被IDE的debug功能折磨了一天)
  • 化简表达式的部分拆解成为几个流程,尽可能降低耦合
  • 重新修改Debug部分模块
  • 减少直接对对象内部的控制,中间添加接口

代码分析

首先是重构后的复杂度分析

MethodCogCev(G)iv(G)v(G)
Box.Box(T)0111
Box.getData()0111
Box.setData(T)0111
Debug.completeDebug()3133
Debug.debug(Expr)1122
Debug.debug(Factor)12177
Debug.debug(Term)1122
Debug.nope()0111
Debug.repeat(String, int)1122
Expr.Expr()0111
Expr.add(Expr)9166
Expr.addTerm(Term)0111
Expr.clear()0111
Expr.clone()1122
Expr.compare(Expr, Expr, int)3311313
Expr.getTerms()0111
Expr.outPut()206810
Expr.print(BigInteger[], BigInteger[], BigInteger[])2911212
Expr.replace(String, String)1122
Expr.setTerms(HashSet)0111
Expr.toString()0111
Expr.toString(boolean, boolean)6445
ExprFactor.ExprFactor(Expr, BigInteger, boolean)2133
ExprFactor.clone()0111
ExprFactor.getExpr()0111
ExprFactor.isNegative()0111
ExprFactor.setNegative(boolean)0111
ExprParser.parse(String)3133
ExprParser.separate(String, int)1511011
ExprReducer.reduce(Expr)0111
ExprReducer.reduce(Expr, Boolean)13199
FactorParser.parse(String)2951618
FactorParser.separate(String, int)12167
Func.Func(String, boolean)2211213
Func.getExpr()0111
Func.getSymbol()0111
Func.getVarList()0111
Func.replace(ArrayList)1122
Func.setExpr(Expr)0111
Func.setVarList(ArrayList)0111
FuncHandler.FuncHandler(HashMap<String, Func>)3133
FuncHandler.replace(String, Box)183912
MainClass.main(String[])9155
MainClass.testing(String)0111
Mult.parse(Expr, BigInteger)8455
Mult.parse(Expr, Expr)3133
Mult.parse(Expr, Factor)3144
Mult.parse(Expr, String, BigInteger)8455
NumFactor.NumFactor(BigInteger, BigInteger, boolean)0111
NumFactor.addNum(BigInteger)0111
NumFactor.clone()0111
NumFactor.getIndex()0111
NumFactor.getValue()0111
NumFactor.isNegative()0111
NumFactor.setIndex(BigInteger)0111
NumFactor.setNegative(boolean)0111
NumFactor.setValue(BigInteger)0111
Term.Term()0111
Term.addFactor(Factor)0111
Term.addNum(BigInteger)3333
Term.clone()5155
Term.getFactors()0111
Term.getNegative()13177
Term.getNum()6144
Term.getVar()9656
Term.merge(Term)0111
Term.newFactors(HashSet)0111
Term.replace(String, String)8166
Term.sort()13188
Term.toString()0111
Term.toString(boolean, boolean)1761011
TermParser.parse(String)3133
TermParser.separate(String, int)12167
TermReducer.combine(Expr)14177
TermReducer.extract(HashMap<String, Factor>, boolean, BigInteger, ArrayList)6155
TermReducer.reduce(Term)341913
Testing.calculate(String)0111
Testing.generate()0111
Testing.test()6434
VarFactor.VarFactor(BigInteger, String, Expr, boolean)0111
VarFactor.VarFactor(Expr, boolean)0111
VarFactor.VarFactor(String, BigInteger, boolean)7144
VarFactor.addExpr(Expr)0111
VarFactor.addIndex(BigInteger)0111
VarFactor.clone()2222
VarFactor.getExpr()0111
VarFactor.getIndex()0111
VarFactor.getVar()2851011
VarFactor.isExp()0111
VarFactor.isNegative()0111
VarFactor.setExpr(Expr)0111
VarFactor.setIndex(BigInteger)0111
VarFactor.setNegative(boolean)0111
VarFactor.setVar(String)0111

看起来部分方法,例如化简和输出的方法复杂度仍然很高,但实际上这部分是因为大量调用了Debug的方法,实际在Debug模式被关闭的情况下是不会影响程序内容的

新增内容的修改
  1. 将预处理后的函数中,带入实参
Expr expr = this.expr.clone();
String ans = expr.toString(false, false);
for (int i = 0; i < list.size(); i++) {
    ans = ans.replace("@" + i, "(" + list.get(i) + ")");
}
return ans;

带入的实参要额外带一层括号,以免部分内容优先级出错

  1. 输出标准化的变量因子
if (this.expr != null) {
    Expr expr1 = this.expr.clone();
    expr1 = ExprReducer.reduce(expr1);
    HashSet<Term> terms = expr1.getTerms();
    int i = terms.size();
    if (i == 0) {
        return "exp(0)";
    } else if (i == 1) {
        String exp = expr1.toString(false, false);
        int t = 0;
        BigInteger integer = BigInteger.ONE;
        for (Term term : terms) {
            for (Factor factor : term.getFactors()) {
                if (factor instanceof NumFactor) {
                    NumFactor numFactor = (NumFactor) factor;
                    integer = integer.multiply(numFactor.getValue());
                    if (numFactor.isNegative()) {
                        integer = integer.multiply(new BigInteger("-1"));
                    }
                } else if (factor instanceof VarFactor) {
                    t++;
                }
            }
        }
        if (t == 1 && integer.equals(BigInteger.ONE)) {
            return "exp(" + expr1.toString(false, false) + ")";
        } else {
            return "exp((" + expr1.toString(false, false) + "))";
        }
    }
    // TODO
    // Debug.debug(this.expr);
    return "exp((" + expr1.toString(false, false) + "))";
}
return var;

这其中的提取参数的部分,实际上依旧被修改到expr的方法中,但是这里忘了改了

  1. 通过FuncHandler对表达式字符串进行替换
String ans = funcMap.get(func.getSymbol()).replace(func.getVarList());
waitingMap.put(find, ans);
for (String s : waitingMap.keySet()) {
    // TODO bug ff(x) f(x)
    if (ans.contains(s)) {
        suc.setData(true);
        ans = ans.replace(s, "(" + waitingMap.get(s) + ")");
    }

}

return ans;

前面先提取出对应的实参,并将实参覆盖到预处理后的函数中,生成一个字符串到字符串的映射关系,最后直接进行字符串替换

  1. 重载的toString方法
public String toString(boolean ignoreNum, boolean debug) {
    Expr expr = clone();
    ArrayList<String> list = new ArrayList<>();
    for (Term term : expr.getTerms()) {
        list.add(term.toString(ignoreNum, debug));
    }
    Collections.sort(list);
    StringBuilder sb = new StringBuilder();
    for (String s : list) {
        if (s.equals("0")) {
            continue;
        }
        sb.append(s).append("+");
    }
    if (sb.length() != 0) {
        sb.deleteCharAt(sb.length() - 1);
    } else {
        return "0";
    }
    String s = sb.toString();
    s = s.replaceAll("[+]-", "-");
    s = s.replaceAll("-1\\*", "-");
    return s;
}

这个方法用于对表达式的项进行字典序排序,并最终化简输出,项的toString方法也类似,但是因为起初并不知道IDE在debug时会调用toString方法,导致我经历了极其折磨的一天(IDE输出和jar包输出不一致,但debug输出又一致了......)

除此之外,为了适配exp的化简需要,我将化简时存储的BigInteger类型的指数修改为了因子类型,进而适配指数合并

Bug分析

本次bug共一处,因为重写了expr的输出函数,因此在第一次考虑到仅有0时,会保留0,但重写后忘记这个细节,导致在仅有0作为输出时,会导致输出为空

if (sb.length() != 0) {
    sb.deleteCharAt(sb.length() - 1);
} else {
    return "0";
}

在起初没有else部分的判断,因此输出可能为空

除此之外,还因为toString方法重载,导致IDE运行结果和jar运行结果不一致,最后发现原因是:

  • toString方法会化简表达式
  • add方法有bug,导致只能保留一项
  • 最后导致每次toString化简后,HashSet顺序都会改变,进而导致add保留的项改变,输出结果也改变

经历这一次难忘的debug经历,以后在重载toString方法时,一定要保证内容不被影响

互测bug

发现的同学的bug仅一处:

  • 在函数中调用指数函数,会发现指数函数被替换到原表达式中,且此时的指数函数化简方法和原表达式中的化简方法不同
  • 在函数的化简方法中,如果出现1-1,会导致无输出,如果直接在原表达式中1-1,并不会出现无输出的情况
  • 但是在函数中的指数函数中出现无输出情况,则会导致替换到原表达式后,解析出错,进而报空值BigInteger的异常

总的来说,这个bug的触发条件比较复杂,主要错误原因是自定义函数的解释过程和表达式解释过程不同,而出现的bug

评测机:

在第一次作业时为了更好的进行互测,我写了一个简单的评测机,其中测试点是根据规则生成的,但是因为没有总的限制测试点的复杂度,导致可能出现部分复杂度过高的情况,反而导致最后程序运行时间太长,影响测试效率,因此我一并改为超时即停止

具体生成代码比较长,在这里只介绍一些我为了优化测试点的设计:

  1. 限制嵌套深度,限制测试点长度

这部分都是通过一个有上下限的非线性函数表示,根据深度,长度逐渐增加,函数得到的生成概率会快速降低,最终到达上限时为0

  1. 限制指数大小

这部分有正态分布生成,0-8的在数值上的分布为0.5的间隔,但是因为正态分布的性质,导致0的概率显著高于8

使测试点的能够大概率测试出包含0的特殊情况,同时不会导致复杂度过高

  1. 限制数字大小

因为要测试BigInteger的性能,因此我和第一个限制方法是一样的,即数字长度越高,继续延伸的概率也越低,最后生成的数值大部分分布于2-10位,小概率会出现20+甚至50+的特殊情况,以测试大数的运算效果

优化

起初在考虑指数函数时,本认为提取公因式不一定能带来很大的性能提升,因此没做提取公因式

除此之外,程序的优化主要有:

  • 对函数的预处理,使得带入后的运算时长显著下降
  • 复用指数函数和变量因子,使程序设计复杂度下降
  • 重构部分代码,使得耦合度下降

第三次作业——求导,函数复用,多变量

实验思路

第三次作业的新增要求为:

  • 在函数定义中可以引用之前以定义的函数(导致函数定义部分可能出现多变量)
  • 求导因子

在实现函数部分时,因为我的处理方法是先进行预处理,并且会将函数表达式部分解析

因此整体代码可以复用,只需要将每一次处理后得到的函数对进入到FuncHandler中再次处理下一个函数

即可对新的函数中调用的函数进行替换,因此不需要进行大改

而关于求导因子,具体实现方式参考了第二次实验中的代码,唯一区别在于变量求导有一点区别

总的来说,如果只追求正确性,第三次作业的要求并不多,在前两次兼顾到拓展性的情况下,并不需要进行多少修改

架构设计

第三次作业的架构和第二次没有变化,唯一区别在于表达式,项和因子都添加了一个求导方法,用以递归调用求导

而对于整体流程:

  • 读入文本
  • 预处理自定义函数
  • 将函数结合实参进行字符串替换
  • 解析表达式,建树
  • 合并表达式,化简
  • 输出表达式

也没有明显变化

代码分析

  1. 解析自定义函数
int n = Integer.parseInt(scanner.nextLine());
for (int i = 0; i < n; i++) {
    String funcS = scanner.nextLine();
    funcS = funcS.replaceAll("[ \t]", "");
    FuncHandler funcHandler = new FuncHandler(funcMap);
    Box<Boolean> suc = new Box<>(true);
    while (suc.getData()) {
        suc.setData(false);
        funcS = funcHandler.replace(funcS, suc, true);
    }
    Func func = new Func(funcS, true);
    funcMap.put(func.getSymbol(), func);
}
  1. 处理导数因子
Expr expr = ExprParser.parse(subsub.substring(i + 3, tj - 1));
expr = expr.derivation();
return new ExprFactor(expr, index, isNegative);
  1. 表达式,项,因子的求导
public Expr derivation() {
    Expr expr = new Expr();
    for (Term term : terms) {
        expr.getTerms().addAll(term.derivation().getTerms());
    }
    expr = ExprReducer.reduce(expr);
    return expr;
}
public Expr derivation() {
    Expr expr = new Expr();
    ArrayList<Factor> list = new ArrayList<>(factors);
    for (int i = 0; i < list.size(); i++) {
        Factor factor = list.get(i);
        Term term = factor.derivation();
        for (int j = 0; j < list.size(); j++) {
            if (i == j) {
                continue;
            }
            Factor factor1 = list.get(j);
            term.addFactor(factor1);
        }
        expr.addTerm(term);
    }
    return expr;
}
public Term derivation() {
    Term term = new Term();
    Expr expr1 = expr.derivation();
    ExprFactor exprFactor = new ExprFactor(expr1, BigInteger.ONE, isNegative);
    term.addFactor(exprFactor);
    return term;
}
public Term derivation() {
    Term term = new Term();
    if (expr == null) {
        if (var.equals("x")) {
            NumFactor numFactor = new NumFactor(index, BigInteger.ONE, isNegative);
            VarFactor varFactor = new VarFactor(var, index.subtract(BigInteger.ONE), false);
            if (varFactor.index.equals(BigInteger.ZERO)) {
                term.addFactor(numFactor);
            } else {
                term.addFactor(numFactor);
                term.addFactor(varFactor);
            }
        }
    } else {
        Expr expr1 = expr.derivation();
        ExprFactor exprFactor = new ExprFactor(expr1, BigInteger.ONE, isNegative);
        term.addFactor(this.clone());
        term.addFactor(exprFactor);
    }
    return term;
}
public Term derivation() {
    NumFactor factor = new NumFactor(new BigInteger("0"), new BigInteger("1"), false);
    Term term = new Term();
    term.addFactor(factor);
    return term;
}

分别是表达式,项,表达式因子,变量因子,参数因子的求导方法,其中变量因子与实验中的代码有一定区别,主要是因为我的变量因子包含指数和指数函数

Bug,优化

这一次作业并没有发现什么bug,同时目前了解到的优化区间主要在于如何快速求导

体会

作为少数没有参加OOpre的学生,第一次作业带给我的压力还是很大的,但多亏之前曾因为兴趣自学过java的基本用法和git等工具的用法,因此并没有在第一次作业直接被卡主。但是缺少面向对象程序设计经验的缺陷却是难以避免的,从程序设计课程的取巧式编程,到数据结构的结构化编程,再到现在的面向对象,在程序设计上的思路和规范发生了一次次的改变。还记得当时在程序设计课程上,还曾用尽可能取巧的方法,仅解决题目给出的单一问题,那是不会考虑这一段代码之后能否修改,能否迭代,只需要简单的十几行到几十行就可以满足题目需求。再到数据结构,终于要构思程序的结构,要保证每一次运算的意义。而今进入面向对象课程,仅仅考虑程序的大致框架已经难以满足迭代开发的需求,当数百行代码互相勾结在一起时,牵一发而动全身,让人无从下手。在第一次作业中,与其说是面向对象编程,不如说是包含着数据结构的面向过程编程。整个流程耦合度极其高,尽管设计时总想着,这一个个模块之间可以有单调的关系,但实践时却总是缺少这样的一个思路和警惕。当最后终于完成全部功能时,却发现这牵连在一起的毛线球已经难以挽救。

总的来说,第一单元作业对我来说,是一次对OOpre的补课,通过这一次迭代开发的体验,让我熟悉了一下OO课程的流程,同时学习了面向对象设计的思路,并有了一定经验。

未来方向

在这次的开发训练中,有如下潜在的发展方向

  • 压缩运行时间限制,记入性能分,考虑优化程序运行时长(之前互测时的一个同学,程序运行时间复杂度显著高于其他人)
  • 考虑增加第三次的任务,目前第三次的开发量远远小于前两次
  • 将程序复杂度参数也记入性能分,提倡降低方法间的复杂度
...全文
79 回复 打赏 收藏 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

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

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