309
社区成员
发帖
与我相关
我的任务
分享| 类名 | 属性个数 | 方法个数 | 总代码行数 | 设计考虑 |
|---|---|---|---|---|
| Calculate | 1 | 6 | 74 | 多项式存储(HashMap)与计算 |
| Expr | 1 | 2 | 20 | 表达式类,可以返回一个包含项的 List |
| ExpressionFactor | 2 | 2 | 15 | 表达式因子 |
| Lexer | 3 | 6 | 52 | 词法分析,识别 token |
| Main | 0 | 1 | 14 | 程序入口 |
| Number | 1 | 2 | 14 | 数字因子 |
| Output | 0 | 3 | 85 | 输出格式化,将项转化为字符串,兼顾一定的长度优化 |
| Parser | 1 | 5 | 126 | 递归下降解析器 |
| Term | 2 | 3 | 27 | 项(内部符号+因子列表) |
| Variable | 1 | 2 | 14 | 变量因子 |
| 总计 | 12 | 32 | 444 |
数据来源:Statistic 插件 + 人工核算
| 类名 | 方法名 | 圈复杂度 |
|---|---|---|
| Lexer | next() | 12 |
| Parser | parseFactor() | 15 |
| ... | ... | ... |
数据来源:Metrics Reloaded 插件的 Method metrics 报告
分析:Lexer.next() 与 Parser.parseFactor() 的圈复杂度比较高(>10),包含较多分支逻辑。前者需要识别数字、运算符、字母等不同词素;后者则根据当前 token 分发到不同的因子解析逻辑。我在后续作业中已被拆分为多个小方法,既是迫于 checkstyle 检查,也是为了提升可读性和可维护性。
add、mul、pow)均围绕唯一的私有字段 terms(HashMap<Integer, BigInteger>)进行操作,方法之间共享数据,内聚性高。next、peek、hasNext)共同维护输入字符串的状态,内聚性较好。但 next() 方法内部混合了数字、运算符、标识符的解析逻辑,可拆分为独立方法(如 parseNumber、parseIdentifier),进一步提升内聚性。parseFactor 方法承担了多种因子的创建职责(数字、变量、括号表达式),导致单一方法内聚性下降。在后续作业中,已将其重构为独立的 parseExpressionFactor、parseVariableFactor 等方法。使用 IDEA 的 Analyze → Analyze Dependencies 功能观察类之间的依赖关系:
Lexer、Expr、Term、Number、Variable、ExpressionFactor 等多个类,耦合度较高。这是解析器需要构建 AST 所必需的设计耦合。HashMap 和 BigInteger,耦合度极低。Calculate 及其内部类 Term,耦合度中等,但职责清晰(仅负责格式化)。
优点:说实话我觉得没什么优点,正常递归下降的架构差不多就是这样,互测的时候看别人代码结构也差不多。真要说的话就是用HashMap来处理表达式的存储保证性能,在Output里面还考虑到了正项提前
缺点:第一次作业我没有构造单项式和多项式类,直接暴力写了个Calculate类负责构建一个装项的HashMap,同时也负责多个HashMap之间的各种运算。这部分功能本应当拆开为单项式类和负责计算的多项式类的,被我耦合在了一起
| 类名 | 属性个数 | 方法个数 | 总代码行数 | 设计考虑 |
|---|---|---|---|---|
| ExpFactor | 2 | 2 | 2 | 指数函数因子 exp(...)^n,考虑了指数为 0 和参数为 0 的特殊情况 |
| Expr | 2 | 2 | 23 | 表达式类,包含项列表,用了 Cache 缓存 |
| ExpressionFactor | 2 | 2 | 15 | 括号因子 (expr)^n |
| FuncFactor | 2 | 2 | 19 | 函数调用因子 f(x),从 Main 获取函数定义并代入 |
| Lexer | 3 | 6 | 76 | 词法分析,支持数字、运算符、exp、f、[() ? :] 等 token |
| Main | 1 | 3 | 32 | 程序入口,读取函数定义和表达式,存储函数多项式 |
| Monomial | 3 | 7 | 45 | 单项式,系数、指数、指数参数(用于 exp 嵌套) |
| Number | 1 | 2 | 14 | 数字因子 |
| Output | 0 | 4 | 145 | 输出格式化,支持有关 exp 优化(尝试提取公因子来优化长度) |
| Parser | 1 | 12 | 210 | 递归下降解析器,支持表达式、项、各种因子 |
| Poly | 3 | 12 | 209 | 多项式表示(Map<MonomialKey, BigInteger>),支持加法、乘法、幂、代入等运算 |
| SelectFactor | 4 | 2 | 24 | 选择运算符 [(A?B):C:D],根据 A 与 B 是否相等返回 C 或 D (注意这里第一步只展开 A B) |
| Term | 3 | 3 | 33 | 项(内部符号+因子列表),缓存计算结果 |
| Variable | 1 | 2 | 14 | 变量因子 x^n |
| 总计 | 28 | 61 | 862 |
数据来源:Statistic 插件 + 人工核算
| 类名 | 方法名 | 圈复杂度 | 说明 |
|---|---|---|---|
| Lexer | next() | 15 | 识别数字、运算符、exp、f、=、==、[? :] 等,分支较多 |
| Poly | mul() | 11 | 涉及两层循环和嵌套 expArg 合并,逻辑复杂 |
| Output | formatMonomial() | 14 | 优化单项式输出,基础情况较多 |
| Output | isFactor() | 17 | 判断字符串是否为因子的复杂正则匹配,类别较多 |
数据来源:Metrics Reloaded 插件的 Method metrics 报告 + 人工核验
分析:Lexer.next() 的圈复杂度仍然较高, Parse.parseFactor() 的复杂度有所降低; Lexer.next() 可进一步拆分为独立方法。新增的 Output.isFactor() 由于需要处理各种因子的正则表达式情况,所以复杂度也很高。
add、mul、pow、substitute 等)均围绕内部的 terms Map(存储 MonomialKey → 系数)进行,内聚性高。内部类 MonomialKey 专门用于键的封装,职责单一。next() 方法仍混合了多种 token 的识别逻辑,可进一步拆分。parseFactor 被拆分为多个独立方法(parseExpressionFactor、parseVariableFactor 等),每个方法只负责一种因子的解析,内聚性比第一次作业显著提高。arg 和缓存,方法 toPoly 从 Main 获取函数定义并代入,职责单一,内聚性高。toPoly 根据前两个因子的相等性选择后两个因子,职责明确,内聚性好。使用 IDEA 的 Analyze → Analyze Dependencies 功能观察类之间的依赖关系:
Lexer 以及多个具体的 Factor 实现类(Number、Variable、ExpFactor、FuncFactor、ExpressionFactor、SelectFactor),耦合度较高。这是解析器需要构建 AST 所必需的设计耦合,但可以通过引入工厂模式进一步降低。Main 的静态方法 getFunctionPoly(),形成了静态耦合。这种耦合使得 FuncFactor 无法独立测试,且限制了函数定义的灵活性。可改进为通过构造函数注入函数定义。Monomial 和 MonomialKey,但内部类封装良好,与外界耦合度低。Poly 和 Monomial,职责单一,耦合度适中。
优点:在 Parser 类中,将每种因子的解析独立为私有方法,降低了 parseFactor 的复杂度,提高了可读性; Output 在格式化 exp() 的时候,尝试提取公因子优化
缺点:最严重的一个问题就是在输出的时候没有设计缓存计算,这样在嵌套许多层相同的因子的时候,需要转化成字符串的项呈指数级增长,导致我在规定时间内没能计算出结果,这个问题在第二次作业互测没有人叉我,导致第三次作业我因为这个 bug 被叉了十次。说完这个,其次的缺点就是在优化方面,在第二研讨课中,有同学分享了纯提公因数策略下的最优遍历策略,这个我没有考虑到,如果有设计特别精妙的系数整理,那我可能也会超时,不过这样的样例在互测数据限制下不会出现;此外状压 DP 和贪心算法我也都没用,用了也是AI写,怕有副作用
| 类名 | 属性个数 | 方法个数 | 总代码行数 | 设计考虑 |
|---|---|---|---|---|
| DerivativeFactor | 2 | 2 | 24 | 导数因子 dx()、dy()、grad(),对表达式求偏导或梯度 |
| ExpFactor | 2 | 2 | 24 | 指数函数因子 exp(...)^n,处理指数为 0 和参数为 0 的特殊情况 |
| Expr | 2 | 2 | 23 | 表达式类,包含项列表,使用缓存避免重复计算 |
| ExpressionFactor | 2 | 2 | 15 | 括号因子 (expr)^n |
| FuncFactor | 3 | 2 | 21 | 函数调用因子,支持普通函数 f(x) 和递归函数 f{n}(x),从 Main 获取定义并代入 |
| Lexer | 3 | 6 | 74 | 词法分析,支持数字、运算符、exp、f、dx、dy、grad、[? :]、{ } 等 token |
| Main | 2 | 3 | 59 | 程序入口,读取函数定义(包括递归函数),存储普通函数和递归函数的多项式表示 |
| Monomial | 4 | 8 | 51 | 单项式,系数、x指数、y指数、指数参数(用于 exp 嵌套),支持二元变量 |
| Number | 1 | 2 | 14 | 数字因子 |
| Output | 1 | 4 | 156 | 输出格式化,支持 exp 优化(提取公因子),支持二元变量输出,增加缓存避免重复格式化 |
| Parser | 1 | 12 | 184 | 递归下降解析器,支持表达式、项、各种因子,新增导数因子的解析 |
| Poly | 2 | 14 | 254 | 多项式表示(Map<MonomialKey, BigInteger>),支持加法、乘法、幂、代入、偏导(x和y)、梯度等运算 |
| SelectFactor | 4 | 2 | 24 | 选择运算符 [A?B:C:D],根据 A 与 B 是否相等返回 C 或 D |
| Term | 3 | 3 | 34 | 项(符号+因子列表),缓存计算结果 |
| Variable | 2 | 2 | 20 | 变量因子,支持 x 和 y,存储变量名和指数 |
| 总计 | 34 | 60 | 980 |
数据来源:Statistic 插件 + 人工核算
| 类名 | 方法名 | 圈复杂度 | 说明 |
|---|---|---|---|
| Lexer | next() | 19 | 新增 dx、dy、grad、{、} 等 token,分支进一步增加 |
| Main | main | 11 | 新增了递归函数之后,对于输入的判断分支也增多,我都在main里面进行处理,复杂度提升 |
| Output | formatMonomial() | 27 | 优化单项式输出,需同时处理 x 和 y 指数,以及 exp 嵌套 |
| Output | isFactor() | 16 | 判断字符串是否为因子的复杂正则匹配,新增对 f{...}(...) 等格式的支持 |
数据来源:Metrics Reloaded 插件的 Method metrics 报告 + 人工核验
分析:Lexer.next() 由于新增了导数相关的 token,圈复杂度继续上升;Output.formatMonomial() 因因子种类较多,基础情况较复杂,圈复杂度较高,但这是功能扩展的必要代价。
add、mul、pow、substitute、deriveX、deriveY 等)均围绕内部的 terms Map 进行操作,内聚性高。parseFactor 被拆分为多个独立方法,每个方法只负责一种因子的解析,内聚性良好。新增的 parseDerivativeFactor 方法职责单一。arg 和 idx 两个字段,toPoly 根据 idx 从 Main 获取对应函数定义并代入,职责明确。expr 和 type,toPoly 根据 type 调用 Poly 的求导方法,职责单一,内聚性好。Main 的静态方法 getFunctionPoly() 和 getRecFunctionPoly(),形成静态耦合。递归函数定义也通过静态数组传递,不利于测试和扩展。deriveX 和 deriveY 方法调用了 expArg 的求导方法(expArg.deriveX()),形成了对 Poly 自身的递归依赖,但这是实现链式法则的自然方式,耦合度可接受。outputCache 缓存,避免了重复格式化,同时减少了对 Poly 的重复访问,耦合度适中。
优点:Output 类中增加了 outputCache 缓存,避免重复格式化相同多项式,提升了性能,解决了第二次作业中输出超时的问题; Monomial 同时支持 x 和 y 指数,以及嵌套 expArg ,统一了多远多项式和指数函数的表示。
缺点:在计算递归函数的时候,硬编码了 recFunctionPoly 数组长度为 6 ,且递归展开只生成到 f{5} ,缺乏一定的通用性; SelectFactor 的前两个因子如果非常复杂,时间上会有一定浪费,可考虑加入结构比较或者惰性求值
我主要想谈谈自己第一次作业到第二次作业的部分重构。
在第一次作业中,我自己设计的 Calculate 类负责表达式中每一项的系数和指数的存储,以及两个表达式的各种计算。这样的架构设计在第一次作业中表现优异,没有暴露出来问题,但是在第二次作业时,我发现如果再将存储和计算写在一块儿,就没能做到功能的分离,其次如果再叫 Calculate 类就不合适了。在AI的指导下,我改为了 Monomial 和 Poly 的架构,这样两个类都不会很臃肿,且各自功能相对独立。
关于自定义的新迭代场景:如果还要支持三角函数 sin 和 cos ,并支持先前的求导、计算等操作,那么我应当新增两个三角函数因子类,然后在 Lexer 和 Parser 中要新增三角函数的识别,其次要在 Monomial 和 MonomialKey 中增加 funcType 字段并修改 Poly 使其支持三角函数的相关计算和求导规则。总修改量不是很大,体现出我的设计具有一定的可扩展性。
我先把这个bug给出来:
1
f(x)=exp(exp(exp(exp(exp(exp(exp(x)))))))
0
f(f(f(f(x))))
这个bug在我第二次作业的时候就有,不过当时互测没有被测出来,导致我第三次互测因为输出的时候字符串替换没有做缓存处理被hack了十次。具体来说,在 Output 类的 getOutput 方法里,对于同样的项,我每次都要重复处理,没有设计缓存计算,从而在多层嵌套的时候超时了。出现 bug 的的方法其实复杂度不是很高,代码行数也不多,只是没有设计好这个性能优化,导致出现了问题。关于如何降低代码的复杂度,我认为可以依据功能提炼出不同的方法,这样可以减少单个方法的复杂度,让代码更加易读。
在互测时,我尝试找到别人 bug 的策略,首先是针对我自己代码在公测截止前做的优化,看看别人有没有做。如果别人没有采用我考虑到的优化,那么就很可能在这上面栽跟头。在第二次互测的时候,我就用了这个策略,成功找到了别人使用深克隆超时的问题。然后就是把代码丢给 AI ,看看有没有什么一般性的问题,不过我试了很多次, AI 都没能构造出可以成功 hack 别人的样例。最后就是读别人的代码,当然也不用全读,比如说底层的因子类和文法解析类,大体上就可以不用看了,容易出问题的就是输出优化方面和选择式因子的计算相关,这些方面可以细致看一下,在第二次互测中,我也找到了别人分析选择式因子时候全部展开而导致的超时问题。
其实我的代码在输出长度上没做什么优化,只有针对表达式的正项提前和针对 exp 的提公因子。在提公因子这一步中,我会尝试找到 exp 内表达式中各项系数的最大公因数,然后将其提取到外面的指数中,并与提取前进行对比,选择长度较短的那个。因为我做的优化比较简单,所以还是保持了功能上的正确性;这个方法放在 Output 类中,基本也能保持代码的简洁性与易读性。
在第二次研讨课中,有同学针对提公因子的遍历优化和状压 DP 介绍了许多,但是我只听懂了前面公因子遍历到 gcd/10 的优化,后面的优化确实不会。我不确保自己如果用 AI 写了这个方法,会不会引出别的 bug 。
在第一次作业中,我在正确性和性能优化上的 AI 使用率分别是 40% 和 20% 。
在第二次作业中,我在正确性和性能优化上的 AI 使用率分别是 50% 和 30% 。
在第三次作业中,我在正确性和性能优化上的 AI 使用率分别是 40% 和 20% 。
在生成作业代码之外,我曾试过 Gemini 、 Deepseek 来辅助读代码、找 bug 、写博客作业。 AI 在生成代码方面特别厉害,基本没有什么问题,但是在找 bug 的时候,基本分析不出来程序的问题,很容易被代码中的文法处理部分所混淆;如果能提供测试样例,定位 bug 并修复的能力又很强。
第三次作业的摇光星部分代码配的注释有些像 AI 写的;天玑星的 Main 方法中甚至有如何运行的方法提示,感觉也是 AI 写的。
第一单元聚焦于文法分析和递归下降的解析方法,在类之间的架构设计上,如果摸清了递归下降的结构,那么设计起来不是特别难,但是真要自己实现好其中的各种递归调用和细节,确实很有挑战。我在上完 oopre 之后就没写过面向对象的代码,在第一次作业刚开始的时候,甚至有很多语法知识都忘了,起步异常艰难,在自己写了底层的因子类和计算类之后,又有一段时间无从下手,尤其是 Lexer 和 Parser 的搭建,确实需要 AI 的帮助,因为我脑袋里对于整个流程还是模模糊糊的状态;后来在第一次作业成型后,后续两次的迭代相比之下就要简单一些,不过更多的困难是在优化于括号处理上,我也因为存储结构的选择、输出的优化而被坑了许多次。
这一单元的作业让我体会到了 AI 的强大,但是也暴露了 AI 的不足。我在考虑之后的作业自己多投入一些时间来研究正确性或者性能上的细节,这样在应对公测或者互测,我都有更足的把握。在打地基的阶段,还是自己多写一点练练手比较好,之前我 oopre 大部分是纯手写的,相比于 AI 生成部分代码,纯手写明显对于代码和各种细节的处理有更强的把控能力。
上来就让我们从零开始搭建文法分析、递归下降我觉得还是太难了,建议在实验课更好地引导我们怎么写 Lexer 和 Parser ,这个相比于因子、项的构建我感觉要难得多。
此外,我认为可以略微提高性能的重要性,目前 AI 写代码在正确性上基本没有问题,那么如果我们利用 AI 进行性能优化,就能发现更多的问题,比如有很多同学没有注意到的缓存计算、表达式短路、状 DP 复杂度控制等。