2024北航面向对象(OO)第一单元总结

furina的动物朋友 学生 2024-03-20 16:54:10

1.程序结构分析

类UML图

img


左上方的部分用于对输入进行解析并生成后缀表达式,参考了training中的代码,Parser分析语法、Lexer分析词法,剩下的ExprTermNumberVariablePower等都是递归下降过程中需要处理的成分,其中比较特殊的有:Function表示自定义函数、ExpFunc表示指数函数、Differential表示求导因子;右侧的三个类负责将后缀表达式生成基本项并进行运算:PolynomialMonomial分别是表达式和基本项,PolyCal负责计算表达式;下方的两个类都是静态的类,无需实例化便可以调用其中的方法:FunctionMode用于存储自定义函数的变量以及定义式,Printer 用于输出最终的结果。

各部分的代码量

img


可以很明显地看出来PolyCalParserPrinter三个类的代码冗长。其中PolyCalPrinter两部分主要是因为偏向面向过程,而Parser中则是因为有三个方法中都有很相似的代码,应该专门写一个满足该功能的方法进行复用。

#这三个方法分别用来处理自定义函数、求导因子和指数函数,有很高的相似度
private String getFunction() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(expression.charAt(pos++));//读入fhg
        stringBuilder.append(expression.charAt(pos++));//读入左括号
        int flag = 1;
        while (pos < expression.length() && flag != 0) {
            if (expression.charAt(pos) == '(') {
                flag++;
            } else if (expression.charAt(pos) == ')') {
                flag--;
            }
            stringBuilder.append(expression.charAt(pos));
            ++pos;
        }
        return stringBuilder.toString();
    }
    
    private String getDiff() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(expression, pos, pos + 3);
        pos = pos + 3;
        int flag = 1;
        while (pos < expression.length() && flag != 0) {
            if (expression.charAt(pos) == '(') {
                flag++;
            } else if (expression.charAt(pos) == ')') {
                flag--;
            }
            stringBuilder.append(expression.charAt(pos));
            ++pos;
        }
        return stringBuilder.toString();
    }
    
    private String getExpFunc() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(expression, pos, pos + 4);
        pos = pos + 4;
        int flag = 1;
        while (pos < expression.length() && flag != 0) {
            if (expression.charAt(pos) == '(') {
                flag++;
            } else if (expression.charAt(pos) == ')') {
                flag--;
            }
            stringBuilder.append(expression.charAt(pos));
            ++pos;
        }
        return stringBuilder.toString();
    }

类复杂度

img


类复杂度高度集中在分析表达式、分析自定义函数、计算表达式和输出表达式的部分。与上述代码冗长部分有很大程度上的吻合。

类内方法复杂度

img


img


img


类内复杂度高的地方多出现在需要递归调用的方法内

2.架构设计体验

2.1 第一次作业

刚读完指导书时感到十分茫然,不知道这么庞大的工程要从哪里入手。不过好在课程组在课程平台以及公众号提供了示例代码。在看完training以及公众号中的示例代码后,又考虑到后期需要扩展多层括号嵌套,于是我采用了递归下降解析表达式的方法。由于training代码中要求我们实现后缀表达式的生成,于是我选择引入单项式(大家所说的基本项)利用栈计算后缀表达式的方法来将表达式展开。因此,从hw1开始我的架构就可以明确的分为两部分:一部分参照training部分提供的递归下降方法解析,另一部分专门设计cal类来处理“+-*/^”的计算并打印输出结果。

hw1类图

img


除了ParserLexer解析器外,Monomial是基本项类、Polynomial是表达式类、PolyCal是表达式计算类。

2.2 第二次作业

hw2大概是第一单元最困难的一次作业。自定义函数利用实参替换形参就可以解决,重要的是如何把指数函数融入后缀表达式计算的过程。老师在鼓励大家去重构优化自己的架构,身边的同学也在尝试去重构。在这样的氛围中我也开始思考如何去优化自己的架构。起初我想要去优化掉后缀表达式计算的过程,因为可以递归下降分析表达式的过程直接实现计算。在思考了一段时间后我决定延续我hw1中的 架构,不做重大改动。首先,我认为优化掉后缀表达式后会导致分析表达式成分与计算表达式的过程发生一定程度上的耦合,有可能出现意想不到的问题。其次,我发现了“+-*/^”是二元运算符,而指数函数的本质是一个一元运算符,因此指数函数也可以用后缀表达式来表示,例如exp(x)的后缀表达式就是“x e”(用e来表示指数函数),这样指数函数便也可以完美融入后缀表达式计算的过程了。

hw2类图

img


hw2在hw1的基础上添加了ExpFuncFunction类来解析指数函数和自定义函数。除此之外还可以发现右侧多了两个静态类。其中FunctionMode中用HashMap来记录自定义函数的种类、变量类型以及定义式;Printer专门用来输出结果,实际上就是吧hw1中PolyCal中输出结果部分的代码转移到了Printer类中来使PolyCal类的功能更加专一且没那么臃肿。

2.3 第三次作业

hw3添加了求导的过程并允许了自定义函数的定义过程中可以包含自定义函数。经历了hw2的思考,hw3便比较轻松了。由于在hw2中我选择在递归下降的过程中处理自定义函数并返回一个factor,于是它本身就能够实现处理用自定义函数定义的自定义函数。求导因子的处理方法和hw2中处理指数函数的方法一样:将其视为一元运算符加入后缀表达式计算。(dx(x)表示为“x d”)

hw3类图

img


hw3增量比较少,在表达式解析层面增加了Differential求导因子;计算层面在PolyCal中加入了用于处理求导运算的方法Dervation();输出方面在Printer中加入了提取公因数的优化方法Extraction()

2.4 新的迭代情景

假设要在新的迭代中加入三角函数。首先,解析含有三角函数的表达式的过程和解析指数函数基本一样。将sincos视为一元运算符,生成的后缀表达式为“x s”或是“x c”。在运算层面,基本项中需要添加两个容器来存储三角函数中的表达式。和指数函数相比,三角函数的求导会更加复杂,因此计算求导的过程需要修改。

3.bug分析

3.1 第一次作业

第一次作业由于引入的运算种类少,所以没有发现什么正确性层面的bug。不过在测评自己代码的过程中我发现我的程序在运行含有7、8次方的长表达式时会很慢得到结果,这存在很大的TLE风险。经过设置断点调试,我发现了在幂函数指数运算后进行合并同类项的运算速度会很慢,因为要遍历几百万个基本项。于是我在每种运算("+-*/^")后都引用了一次合并同类项的方法,这使得程序的运算速度达到了很大的提升。

3.2 第二次作业

第二次作业由于加入了两种新的运算,出现了一些正确性层面的bug。首先在后缀表达式生成层面,我最开始在自定义函数实参替换形参的过程中没有考虑到指数函数的表达式exp中的x也会被替换掉,导致exp指数函数的表达式不能被正常读取,出现报错;在计算后缀表达式层面,由于加入了exp指数函数,基本项的复杂度升高了很多。在hw1中基本项对象只拥有系数和指数两个属性,而随着exp指数函数的加入,我为它添加了一个ArrayList用来储存exp指数函数中的表达式。这样便会产生表达式由基本项组成、基本项中含有exp表达式的层层嵌套的复杂结构,为表达式计算以及合并同类相都带来了不小的麻烦。在表达式加减法所涉及的方法中我原本使用了浅克隆,但由于基本项的复杂度增高,浅克隆会出现意想不到的错误,将浅克隆替换成深克隆就可以修正这些错误。

3.3 第三次作业

第三次作业在迭代的过程中没有产生什么bug,但在进行提取公因数的性能优化过程中产生了“同时嵌套多层可提取公因数的指数函数,但内层提取的公因数无法被输出”的bug。经过设置断点的调试发现是递归输出逻辑出现问题而导致的bug。

3.4 总结

虽然在第一单元的公测和互测中都没有被测出bug,但在课下编写代码的过程中还是遇到了很多的问题。这些问题集中在程序中面向过程性强、代码冗长的地方,反映出我对java一些底层原理的不熟悉以及面向对象思想的理解不够深入。

4. 他人程序的bug

在hw1中我选择用测评机生成的大量数据去运行别人的代码,取得了不错的成效。但随着测评机被推广,在hw2和hw3的互测中测评机都没有起到什么作用,所以我尝试自己去构造特殊一点的测试数据,比如和0有关的数据。值得一提的是,在hw2中由于表达式的形式变得复杂,能够优化的地方相比较于hw1也多了很多,通过阅读同组同学的代码我发现他们很多都选择了提取公因数这个优化方法,但是他们在优化时却少考虑了一些情况,导致输出不符合指导书中的格式要求。比如exp(-x)exp(x)^-2。想要看懂别人写的代码虽然困难,但仔细阅读后也有可能会收获意外之喜。

5. 优化

5.1 实现

由于hw2因为没有提取公因数丢了性能分,所以在hw3中加入了指数表达式内提取公因数的操作。实现过程偏面向过程,需要对表达式进行遍历,并利用BigInteger.gcd()方法去寻找最大公因数并将提到exp()外面。当然,在某些情景下提取公因数会导致输出长度不变或甚至是变长,因此我又写了一个ExtLen()方法来检测输出的长度,比较提取公因数前后输出字符串的长度,然后决定是否提取公因数。

#要注意去制定合理的长度比较标准:需要考虑到系数为1或-1时的长度该如何计算、是否需要把符号的长度也计算在内等细节。
public static int ExtLen(Polynomial input) { //测量系数长度
        int len = 0;
        for (int i = 0; i < input.getPolynomial().size(); i++) {
            BigInteger coefficient = input.getPolynomial().get(i).getCoefficient();
            BigInteger index = input.getPolynomial().get(i).getIndex();
            Polynomial exp = input.getPolynomial().get(i).getExp();
            if (coefficient.equals(BigInteger.ONE) ||
                    coefficient.equals(BigInteger.ONE.negate())) {
                if (index.equals(BigInteger.ZERO) &&
                        exp.getPolynomial().isEmpty()) { //常数为1的系数需要正常输出
                    len = len + coefficient.toString().length();
                } else {
                    if (coefficient.equals(BigInteger.ONE.negate())) {
                        len = len + 1;
                    }
                }
            } else {
                if (index.equals(BigInteger.ZERO) && exp.getPolynomial().isEmpty()) {
                    len = len + coefficient.toString().length();
                } else {
                    len = len + coefficient.toString().length() + 1; //乘号
                }
            }
        }
        return len;
    }

5.2 正确性与简洁性

注意提取的最大公因数不能是负数,否则会导致输出格式错误,同时要选择合理的方式输出提取出的指数。由于这只是进行简单的优化,代码长度较短,因此并不会对简洁性造成很大影响。

6. 心得体会

在写作业的过程中要多关注讨论区,也要多和身边同学进行讨论,这样可以吸取他人架构中的优点,同时也可以关注到一些自己在阅读指导书过程中没有关注到的细节,在编程的过程中少走弯路。
要调整测试的策略,不能只用测评机无脑运行自己手中的代码。而是要先思考哪些样例会更加特殊、更加接近边界条件。无脑地使用测评机可能会导致代码中隐藏了意想不到的bug同时也会浪费大量的时间,比如在hw3的互测中我的房间有两个人被别人的数据点hack到了,而我用测评机跑了很久却无法发现任何问题。
写程序前要认真阅读指导书中对输入合法性的要求。由于每次作业写的都有点仓促,没仔细去研究指导书中对输入格式的要求,于是很多非法的输入我的程序都可以正常运行。而在互测的过程中我发现了有一些同学对输入的解析非常严格,不符合格式要求的数据都无法解析,这给我评测他们的代码带来了一定的麻烦,同时也给我带来了思考:假如某次迭代要求加入对输入格式的合法性判断(比如不合法输出“WrongFormat”),那么我的程序就会面临很大的考验。所以在接下来的几个单元中我需要更加仔细地阅读指导书来避免这种情况的出现。
权衡好性能与正确性。在优化的过程中很有可能使程序中衍生出新的bug导致正确性丢失,而不做优化又会导致性能分大量丢失(堪比一个强测数据点)。所以我认为要适度进行优化,在不丢失正确性的前提下提升自己程序的性能。

7. 未来方向

希望能够在性能优化上进行一定方向上的引导,在指导书中说一说在哪些地方可以优化或是给出某些优化的方法。
希望能够将评测中的cost设置的更加合理(由于exp的cost太小,室友被exp嵌套的数据hack了好几次)。

...全文
130 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

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

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