302
社区成员
发帖
与我相关
我的任务
分享如果说要用一个词来形容第一单元的表达式解析作业,那一定是“破茧”。从最初面对一长串正则表达式的无从下手,蜕变为能够熟练运用递归下降和 AST(抽象语法树)处理复杂嵌套求导,这不仅是代码量的积累,更是思维方式从“面向过程”到“面向对象”的根本转变。在这篇博客中,我将复盘这三次作业的架构演进、踩过的坑、Hack 别人的快感,以及大模型在这个过程中扮演的角色。
在这三次作业中,我的代码结构经历了剧烈的动荡,最终趋于有序。
代码规模与复杂度演变: 在 HW1 中,我的 Parser 类极其臃肿,单类代码量突破了 300 行。尤其是 parseExpression 方法,控制分支数目(圈复杂度)极高,充斥着大量的 if-else 来处理正负号和项的拼接。这属于典型的“代码坏味道”。到了 HW3,我彻底分离了 Lexer(词法分析)、Parser(语法分析)和 AST Nodes(各类表达式节点)。类的职责变得单一,彼此之间的耦合度大幅下降。
| 类/接口 | 职责考虑 | 优缺点点评 |
Expr | 实现统一的 Factor 接口,充当 AST 根节点或子树。 | 优:抽象良好,统一接口,支持无限嵌套。 缺:作为数据节点承载了过多的计算逻辑。 |
Term | 存放乘积项,负责合并同类项和求导链式法则的组合。 | 优:清晰定义了乘法逻辑。 缺:合并逻辑略显复杂,圈复杂度较高。 |
Sin, Cos | 具体三角因子,重写 derive() 方法。 | 优:多态的完美应用,新增因子极易扩展。 |
DeriveNode | 特殊节点,用于 HW3 的求导操作。 | 优:将求导逻辑从 Parser 中剥离,使架构更清晰。 |
架构反思与多态应用: HW1 时,数据的存储和解析严重耦合,解析器不仅负责切分字符串,还负责计算。到了 HW3,引入了递归下降法并构建了以 Factor 为核心的抽象语法树。这种树状结构和统一接口的设计,使得求导(derive())操作变得非常优雅。为了帮助理解,我制作了以下的 AST 架构示意图:
HW1:跌跌撞撞的“字符串大师”
面对简单的多项式,我试图用极其复杂的正则表达式硬扫全局。结果在处理连续的空白符和前导符号时痛不欲生,架构几乎是平铺直叙的面向过程逻辑。
HW2:重构的阵痛与 AST 的曙光
加入了自定义函数和三角函数后,正则彻底失效。我痛下决心,推翻重写,引入了递归下降法。构建了以 Factor 为核心的抽象语法树。虽然重构的那几个深夜看着 GitLab 上的 commit 记录感到绝望,但这套架构让逻辑瞬间清晰了。
HW3:多变量求导的从容应对
由于 HW2 的底子打得好,HW3 加入链式法则和多变量($x, y, z$)时,架构几乎没有大改。我只需为节点增加 derive() 求导接口和深拷贝机制即可。
如果未来要求加入积分操作或矩阵变量,目前的架构依然稳固。只需新增 IntegralFactor 或 MatrixVariable 类实现统一的 Factor 接口,在 Parser 中注册对应的解析逻辑即可,核心的树状求值和组合逻辑完全不需要修改,符合开闭原则(OCP)。
回顾公测和互测,我的代码并非无懈可击。
HW2 字符串替换的惨痛教训(字符串陷阱):
特征与定位: 强测中挂掉了一个点,Bug 出现在自定义函数解析类中。
原因: 我在处理 $f(x) = \exp(x^2)$ 这类自定义函数时,为了偷懒,直接使用了字符串的 replace() 方法。当传入参数本身包含函数名(例如传入 $\exp(x)$)时,字符串被错误地部分替换,导致语法遭到破坏。
设计反思: 这种 Bug 出现的根本原因是试图在“词法/字符”层面解决“语法/逻辑”层面的问题。正确的做法应该是在 AST 层面进行节点的深层克隆和变量代换,而不是粗暴处理字符串。为了让大家更直观地理解,我制作了 Bug 产生的流程示意图:
在互测环节,我针对不同的架构弱点制定了明确的“打击策略”,这些策略大多结合了被测程序的代码设计结构盲区:
性能与 TLE(超时)陷阱: 针对没有实现惰性求值或深拷贝滥用的代码,我构造了深层嵌套的条件分支或极端指数,例如连续乘法加上 $8^{12}$ 的无意义高次幂运算,成功让房间里几位同学的程序在限定时间内跑不完。
解析器崩溃(Stack Overflow / Exception): 递归下降最怕深度!我利用连续不断的空白符,或者堆叠极多连续的正负号(如 - - - x),精准命中了词法分析器没有正确预处理符号的程序,导致他们直接抛出运行时异常。
边界与数值溢出: 特别喜欢测诸如 $0^0$ 这种数学上的特殊定义,以及连续 22 个 ^8 的极端数据。事实证明,总有人在 BigInteger 转换时考虑不周或发生溢出。
多变量求导漏洞(HW3): 针对多变量函数的顺序替换缺陷,专门构造了 $f(x,y) = x - y$,但在调用时传入相反的参数顺序(如 $y,x$),或者构造极度复杂的嵌套函数来测试偏导数链式法则的断裂点。
为了展示这些 Hack 策略的原理,我制作了以下的看板:

作为新时代的程序员,合理拥抱 AI 是必修课。
代码生成率与辅助定位: 在核心架构(如递归下降逻辑、AST 节点设计)上,AI 生成的代码占比不到 5%。AI 辅助定位逻辑 Bug 往往只能指出表面现象,最终还是依赖我自己在 VS Code 里打断点。
辅助工具与评测机: 我最频繁使用大模型的地方是搭建本地 Python 自动化评测机。我让大模型基于 sympy 库帮我写了一个生成随机嵌套表达式的 Python 脚本,并通过子进程调用我编译好的 Java jar 包进行对拍。这大大减少了手动构造数据的时间。
互测房间的 AI 痕迹: 在 HW2 的互测中,我注意到“天枢星”同学的代码极大概率大量使用了 AI。理由是:他/她的代码中充斥着极其标准但又稍显生硬的、大段大段的 Javadoc(甚至连简单的 getter 方法都写了三行注释);同时,在优化策略部分,存在一些结构极其复杂但实际上并无必要的死循环检测逻辑。
第一单元的跨度非常大,陡峭的坡度主要体现在 HW2 突然引入递归下降。建议可以在研讨课上,更系统地提供一个小型的、基础的“词法分析 + 递归下降语法分析”的 Demo(比如只解析加减法),让大家先有一个具象的认知,从而把更多精力放在架构设计上。
面向对象化作了代码里的对象、接口和多态。未来准备好迎接更硬核的 Debug 挑战。