OO-2026 Unit.1 总结

徐浩然-24371357 2026-03-29 14:37:01

OO Unit 1

1. 架构设计

1. 表达式的解析

Lexer 类:词法分析器

  • 输入为字符串,去除空白字符后逐个解析。
  • 支持识别:
    • 数字
    • 变量:xy
    • 运算符:+ - * ^ ( )
    • 关键字:expdxdygrad
    • 函数记号:f() f{n}()
    • 条件表达式符号:[ ] ? :
  • 提供 next()peek() 方法供解析器使用。

Parser 类:语法分析器

  • 采用递归下降的方法,依据文法调用不同的 parse 方法。

  • 各种 parse 方法将 token 流按照语法规则解析为 Polynomial 对象。

  • 支持的语法结构:表达式(加减)、项(乘)、因子(数字、幂函数、表达式、指数函数、自定义函数、选择式、求导)

2. 表达式的处理

Polynomial 类:多项式类,内部使用 HashMap<Mono, BigInteger>,键为单项式,值为系数。

  • 提供:

    • 基本运算:AddSubtractMultiplyPowerDivide (除数为常数)
    • 复合:Substitute(Polynomial p) 将变量 x 替换为多项式 p,用于表达式函数调用
    • 求导:DriveX()DriveY()
    • 输出:print() 自动处理系数、指数、exp 嵌套
  • 值得一提的设计:

    在构造函数中删除系数为 $0$ 的项,然后所有运算方法按如下设计,便可以保证任意时刻均不会出现系数为 $0$ 的项,无需额外考虑。

    public Polynomial(HashMap<Mono, BigInteger> coefficients) {
        this.coefficients = coefficients;
        Iterator<Map.Entry<Mono, BigInteger>> iterator = this.coefficients.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Mono, BigInteger> entry = iterator.next();
            if (entry.getValue().compareTo(BigInteger.ZERO) == 0) {
                iterator.remove();
            }
        }
    }
    
    public Polynomial Add(Polynomial x) {
        HashMap<Mono, BigInteger> results = new HashMap<>(this.coefficients);
        for (Map.Entry<Mono, BigInteger> entry : x.coefficients.entrySet()) {
            results.put(entry.getKey(), results.getOrDefault(
                    entry.getKey(), BigInteger.ZERO).add(entry.getValue()));
        }
        return new Polynomial(results);
    }
    

Mono 类:单项式类,表示形式:x^a * y^b * exp(Poly),其中 exp(Poly) 表示指数函数,参数为多项式。

  • 提供:
    • 乘法:Multiply(Mono)
    • 复合:Substitute(Polynomial)
    • 求导:DriveX()DriveY()(使用链式法则)
    • 输出:print() 配合系数输出

Function,RecFunction 类:函数类,用于存储和调用函数。

3. 架构设计的评价

类图

img

代码规模

img

可以看出,代码的主要部分为 parser 的解析部分与 Polynomial,Mono 的计算部分。

复杂度

img

可以看出,由于实现的类较少,每个类 WMC 较大。

img

  • print 函数由于考虑了各种输出优化,故认知复杂度较高。
  • lexer.next 函数需要识别各种 token,分支较多,故认知复杂度较高。
  • 大部分方法的 ev 不高(next 方法除外,需要设置全局 token),说明代码较好的实现了封装要求。

总结

  • 我本单元作业的架构可能与大部分同学的架构有所差异:我并没有实现 Factor 接口,对每种因子新建一个类进行存储和管理,而是选择将所有因子都以多项式的结果进行存储和管理。这直接导致我在本单元中仅使用较少的类,而在多项式类中方法繁多。我认为两种架构各自有其可取之处:
    • 对于使用 Factor 接口的架构,其能清晰的表征每种因子的结构,通过接口方法实现统一管理和计算。同时其将解析和运算两个步骤分离,实现功能解耦,能更好的支持选择因子中的短路要求。
    • 对于采用多项式存储的架构,其破坏了因子的原始结构,但是天然支持统一管理和计算。但其将解析和运算同步执行,实现选择式因子时会引入额外的限制,破坏代码的一致性。
  • 在架构设计中有一个纠结的点,我认为函数的定义解析并不应该放在 parser 类中,但是函数的定义解析需要同时用到 parser 类和 lexer 类的方法,而且使用的是同一个 lexer,所以最后还是将函数的定义解析放在 parser 类中,函数类只实现了函数的存储和调用,这种功能分离的架构实在有些奇怪。

2. 迭代过程

1. 第一次迭代

通过对数学意义上的表达式结构进行建模,完成单变量多项式的括号展开,初步体会层次化设计的思想的应用和工程实现。

  • 使用 lexerparser 类解析表达式,之后的迭代中添加新的功能,也需要在 lexer 类内部添加新的关键字,parser 类内部添加新的解析方法,后面不再赘述。
  • parser 类使用递归下降的方法解析表达式,所有解析方法的返回值均为多项式,最后直接调用多项式的输出方法。
  • 此时尚未实现单项式类,多项式采用二元组 $(k,c_k)$ 方式表示 $c_k x^k$,使用 HashMap 维护。

2. 第二次迭代

通过对数学意义上的表达式结构进行建模,完成包含指数函数自定义函数和新增逻辑判断结构的多项式展开与化简,进一步体会层次化设计的思想,并初步接触符号计算中的等价性判定问题。

  • 指数函数:需要新增单项式类,多项式类需要重构。单项式类采用 $x^a exp(poly)$ 方式存储。单项式和多项式都需要支持一些基本的运算。输出逻辑中也涉及到单项式和多项式的相互调用。
  • 逻辑判断结构:笔者认为这个结构反映的是代码的一个重要功能,即判断两个单项式、多项式的等价关系。这个功能不仅用于逻辑判断结构,也可以帮助我们合并同类项,优化表达式长度。我采用重写 equalshash 方法从而实现判等,同时在代码中很小心的删除 0 系数项,避免其带来影响。在强测阶段因为未短路未选择分支而产生计算冗余,而笔者的架构中并未实现解析与计算分离,而是解析后立即返回多项式结果。为解决该问题只能采取一个下策,在 parser 方法中传入参数 calc 表示是否需要计算,如果不需要就只进行形式上的解析占位,然后返回空多项式。
  • 自定义函数:实现自定义函数类,解析时调用 parser 类的方法,将得到的多项式结果存储下来。多项式和单项式类实现 substitude 方法 ,调用函数时将因子代入多项式即可。

3. 第三次迭代

通过对数学意义上的表达式结构进行建模,完成包含求导运算递推函数等复杂结构的多变量表达式的展开与化简,深度体会层次化设计的思想及其在复杂问题中的应用。

  • 求导运算:之前的架构能实现将所有因子转换为多项式,于是只需要在多项式类、单项式类实现求导方法,相互调用即可支持求导运算。
  • 多变量:单项式类改为 $x^ay^bexp(poly)$ ,修改单项式的所有方法即可,注意不要遗漏。
  • 递推函数:递推函数与自定义函数并无太大差异,新增一个 recFunction 类用于存储和查询即可。

总体而言,第三次迭代并没有新增较为复杂的功能,只需在之前的架构上稍加修改即可。这也是我耗时最短的一次作业。

4. 可能的迭代场景

  • 新增对数函数,处理方法同指数函数。
  • 支持虚数运算,可能需要实现虚数类,然后替换原来使用的 BigInteger 类 。
  • 调整指数函数的底数,比如可以设置为 $2^{\text{factor}},4^{\text{factor}}$ 。这可能需要将 Mono 结构中的指数部分改为用 HashMap 存储。不仅如此,我们可以通过变换底数来缩短表达式的长度,自然的引入新的优化问题。

3. bug 分析

在 hw1 中我曾尝试搭建评测机进行代码测试,但发现随机数据并不能有效找出代码在边界情况下的错误,效率较低,因此在后续的作业中放弃了此种做法。

在后续的作业中,我的 hack 思路主要是先结合自己在编写代码过程中认为可能出现的问题(比如表达式等价关系判断)构造一些数据。此外我也会阅读代码容易产生问题的地方(比如输出函数exp括号的问题),针对此构造数据。

hw1

  • 我在本地测试中发现,在多项式类的实现中,每次运算结束我会删除系数为 0 的项,以优化运算性能。但在输出时忽略了在多项式为空时需要输出 $0$ 这个问题。这个问题与架构设计无关,而是没有考虑一些边界情况的特殊处理。
  • 在互测中未发现其他问题。

hw2

  • 在选择式因子中,我的代码没有忽略未选择分支的求值,从而产生巨大的性能损失。
  • 在互测的过程中发现部分同学的代码不能正确的判断表达式的等价关系(比如 $exp(0)$ 和 $1$),我认为这可能是没有忽略系数为 $0$ 的项,从而导致 hash 失效。

hw3

  • 在互测中发现部分同学在迭代过程中没有考虑所有受到影响的方法,比如有一位同学会输出 exp(x*y),这明显是没有考虑到多变量加入对因子判定的影响。
  • 此外,发现部分同学优化没有控制复杂度,针对性的构造了 exp 嵌套的数据。

4. 优化策略

  • 输出时比较常见的优化,比如 $x^1$ 省略指数为 $x$,系数为 $1$ 的项省略系数,移除 $0$ 项
  • 输出多项式时,将正项放在前面可以减少一个字符(‘-’)的长度。这在 hw1 可能不太起眼,但是在后面的作业中多项式层层嵌套,性能可以优化不少。事实上,即使满足正确性要求,hw1 中不添加该优化会被发配到 B 房...
  • $\text{exp(Poly)}$ 可以写成 $\text{exp(Poly/c)}^c$,即提取系数的公因数,这里我只考虑了最大公因数。有同学证明实际上 gcd/1,....10 这十个数都需要才能得到最优的结果。
  • 由于没有进一步实现因式分解等较为复杂的优化,代码的简洁性与正确性未受到较大影响。

5. 大模型相关

本单元作业中主要在检查代码正确性的方面使用了大模型工具,其余部分(代码编写,性能优化)均由自己完成,具体而言:

  • 在 hw1 中使用大模型工具生成评测工具,进行测试。该评测机只能实现数据的纯随机生成,测评结果并不令人满意。

  • 在完成作业后和互测过程中,使用大模型工具分析代码中的错误。大模型很容易找到输出格式的各种错误(我认为这些都是代码编写过程中产生的细节问题,比如结果为0时输出空字符串),也能找到性能方面的问题(比如提供cost计算方法,其能发现选择表达式未短路造成的性能影响)。当然,其结果不一定准确,也会产生一些错误的分析过程。

总体而言,我认为大模型已经能较好的解决目前作业的各种要求。

6. 心得体会

  • 代码的鲁棒性:本单元作业保证了输入数据合法,因而无需过多考虑。但在实际开发过程中,数据的正确性是无法保证的,因而需要加以考虑。此外,从测试的角度出发,应该关注其在各种边界情况下代码的稳定性。
  • 代码架构与迭代:在体系开发过程中,各个类存在复杂的依赖关系,在迭代开发的过程中一定要完整考虑对于各种类,各种方法的影响。这也同时体现了架构的重要性,一个易于扩展的架构需要做到的就是在迭代的过程中修改尽可能少的代码就能兼容新的代码。
  • 优化要适度:在本单元的作业中我仅实现了一些简单的性能优化,故在 hw1,hw3 中均未被hack。在互测的过程中,我发现那些优化的部分往往会引入各种各样的问题,很容易就被 hack。因而在追求优化性能的同时也要考虑其带来的潜在风险。

7. 未来方向

这里吐槽一下,第一次实验课代码解析表达式的方式是通过加减号分隔处理,似乎并没有采用递归下降的方法。我在 hw1 中也采用了类似的方式,但发现其并不能很好的处理多层嵌套的复杂表达式,只能被迫重构代码。感觉可以替换成更标准的递归下降的形式,给出一个大致的架构方便同学们学习。

思考题

  • 检查输入是否符合要求:严格检查 lexer 提供的 token 是否符合预期。对于空白符的处理,可以实现一个函数跳过可能的空白符。
  • 计算合法输入的 cost:类似 toPoly 方法实现一个计算 cost 的函数,不需要进行多项式运算。
...全文
145 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

307

社区成员

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

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