航空系统pta作业集1-3

第一单元三次作业总结Blog

写在前面

课程的第一单元结束了。第一单元,以多项式求导程序为中心,不断增加新的功能,我学到了很多:

  • 熟悉了Java基本语法;
  • 初步建立起了面向对象的设计思路;
  • 认识到了代码可拓展性的重要;
  • 学会了构建自动化测试工具。

说真的,这三周过得挺不容易的。第一次作业我还觉得挺简单,第二次作业就开始头疼了,到第三次作业直接懵了。不过熬过来之后发现,确实学到了不少东西。这篇博客我就随便写写,记录一下这三次作业我都干了啥、踩了哪些坑。

Complexity Metrics(复杂度分析)

因为下面要用到复杂度分析,所以先简单说一下这些指标是啥意思。

我们需要使用的主要是方法和类的复杂度分析。

方法的复杂度分析主要基于循环复杂度的计算。循环复杂度就是看一个方法里面有多少条不同的执行路径。if else多了、for循环多了,这个数就大。数越大说明这个方法越复杂,越容易出bug。

  • ev(G):表示一个方法的结构化程度,值越大说明代码结构越乱,各种break、continue满天飞。
  • iv(G):表示一个方法调用其他方法的紧密程度,值越大说明方法之间耦合越紧。
  • v(G):就是循环复杂度本身,可以理解为测试这个方法需要准备多少种不同的输入。

对于类,有OCavg(类的方法平均复杂度)和WMC(类的总复杂度)两个指标。WMC太大说明这个类干的事儿太多了,该拆分了。

下面我就从程序结构、公测互测、bug分析几个方面来总结一下我的三次作业。

第一次作业

作业要求

实现简单多项式的格式判断和求导。多项式由项相加减组成,项可以是幂函数(比如2*x^3这种)或者常数(比如-5这种)。

我是怎么写的

我的思路很简单:先判断输入的格式对不对,对了就把每一项拆出来,存到ArrayList里,然后挨个求导,最后把结果拼起来输出。

第一次作业代码规模如下图:

(这里放SourceMonitor的截图,显示总行数156行,源码行数132行)

从图上能看出来,代码不多,156行。但问题是,大部分代码都塞在Main类里了,其他两个类基本没啥用。

类图

第一次作业类图如下:

屏幕截图 2026-05-18 121937

这个图画得很简单,因为我的设计就这么简单。Main类啥都干,解析、求导、输出全包了。PolyList就是个装项的列表,PolyNode就是存系数和指数。说实话这个设计挺烂的,但当时觉得能跑通就行,没想太多。

复杂度分析

第一次作业的复杂度分析如下:

(这里放SourceMonitor的复杂度表格)

方法复杂度:

方法名 v(G) 说明
checkFormat 12 格式判断,写了一堆if else
parsePoly 10 解析多项式,也挺复杂
printPoly 15 输出化简,各种情况都要考虑
derive 4 求导,这个倒还好

类复杂度:

类名 OCavg WMC
Main 10.25 41
PolyList 1.0 3
PolyNode 1.0 4

可以看出,Main类的平均复杂度10.25,总复杂度41。正常来说平均复杂度在5以下比较好,我这超了一倍。printPoly方法的v(G)=15,意味着有15种不同的情况要处理,难怪我当时写的时候觉得晕。

PolyList的构造方法和print方法的复杂度很高,因为用了很复杂的if/else条件判断。而PolyList的构造方法条件语句复杂,是因为我对正则表达式不熟,用了很多笨办法。

Bug分析

公测

笔者的程序在公测中没有出现正确性错误,所有测试点都过了。但是由于优化输出的时候忘记-1x中的1可以省略,导致一个点性能分不满。就是输出了"-1x"而不是"-x",少写了一个if判断,扣了分。

互测

笔者的程序在互测中被发现一个bug,原因是笔者在做输出优化时,只考虑了最后只剩一个系数为0的项,如果出现类似0*x3+0*x4这种两个系数为0的项,就会没有输出。

举个例子,输入"0*x+0",我的程序啥也不输出,应该是输出"0"。这是因为我把系数为0的项都跳过了,如果全都跳过,结果就是空的。改起来也简单,加个标志位就行。

笔者在互测中,hack了其他人17次,大部分都是正则表达式的错误使用导致的,主要表现在对输入各种情况考虑不周全。比如有人写的正则匹配不了"1+++2"这种连续加减号的情况。

测试方法

我是这么测试的:

  1. 用python的xeger包生成测试数据**。先写一个正则表达式(尽量覆盖所有合法输入),然后用xeger随机生成字符串,喂给我的程序。

  2. 用python的sympy算正确结果**。用subprocess调用我的java程序,拿到输出,然后用sympy对同一个表达式求导,对比两者是不是一样。这个自动化脚本帮了大忙。

  3. 读别人的代码找bug**。在互测阶段,有2个bug是我直接看别人代码发现的,然后针对性构造数据去hack。

第二次作业

作业要求

在第一次的基础上,增加了sin(x)和cos(x),还加了括号嵌套(比如(2*x+3)这种整体作为一个因子)。输入格式的判断也更严了。

我是怎么写的

第一次的代码根本没法扩展,所以我全部重写了。新的思路是:

  • 搞一个Factor接口,所有因子都实现它
  • Term表示一项,里面有系数和多个因子
  • Expression表示整个表达式,里面有多个Term
  • 用递归下降的方法解析字符串,这样括号嵌套就好处理了
  • 求导用链式法则,每个因子自己知道怎么求导

这次重写花了我两天时间,写的时候觉得好麻烦,但写完再看确实比第一次好多了。

代码规模

第二次作业代码规模如下图:

(放SourceMonitor截图,总行数511行,9个类,38个方法)

代码量一下子涨到511行,是第一次的三倍多。类也从3个变成了9个,我把功能拆分得更细了。

类图

第二次作业类图如下:

屏幕截图 2026-05-18 122132

这个图看起来复杂多了。关键点是:

  • Factor接口统一了所有因子,求导的时候可以多态调用
  • Parser用递归下降处理括号嵌套
  • Expression和Term形成了树状结构,跟多项式的数学结构是对应的

复杂度分析

第二次作业的复杂度分析如下:

(放SourceMonitor的复杂度数据)

方法复杂度(挑几个高的):

类 方法 v(G) 说人话
Parser parseFactor 12 要判断下一个token是啥,分好多种情况
Parser parseTerm 9 解析一项,也有不少分支
Expression toString 14 输出化简,又是各种if else
Term toString 11 输出化简,也是一堆判断

类复杂度:

类 OCavg WMC
Parser 5.6 39
Expression 3.8 23
Term 4.2 21
各种Factor类 <2 <8

从数据能看出来:

  1. Parser类的总复杂度39**,和第一次作业的Main类差不多。递归下降虽然思路清晰,但写出来还是有很多分支判断。

  2. parseFactor方法的v(G)=12**,因为它要决定创建哪种因子:幂函数、sin、cos、常数、括号表达式等等,switch case写了一大堆。

  3. toString方法复杂度还是高**,14和11,和第一次作业一样,输出化简的逻辑就是复杂。

  4. 各种Factor类的复杂度很低**,说明把不同的因子分开写是对的,每个只做自己的事,不乱搞。

Bug分析

公测

公测全过了,性能分也拿满了。这次我在输出上多花了些心思,没再出现漏掉化简的情况。

互测

互测被人发现了一个bug:sin(0)或cos(0)化简不彻底。比如sin(0)求导应该是cos(0)0=0,但我只化简到了"0cos(0)",没有继续化简成"0"。虽然数学上没错,但不符合同学的预期。

还有一个问题:括号嵌套太深会栈溢出。我的解析器是递归的,每层括号就多一层递归。有同学构造了100多层嵌套,我的程序直接崩了。虽然指导书没说支持这么深,但互测里确实会被人搞。

互测中我hack了11个人。发现的bug主要是:

  • 三角函数里面不能有空格,有人写了"sin( x )"就被判非法了
  • 括号匹配没处理好
  • 求导公式记错了,sin求导还是sin(忘了变cos)

测试方法

第二次作业我沿用了第一次的测试脚本,但做了改进:

  1. 写了个随机生成函数,能生成带嵌套括号和三角函数的表达式
  2. 用sympy的expand和simplify让结果更规范,方便对比
  3. 加了性能测试,看每个用例跑多久

第三次作业

作业要求

在第二次的基础上,增加了自定义函数(比如定义f(x)=x+1)和求导算子dx()。还支持函数调用和嵌套。这是最难的一次,看到题目我第一反应是"这怎么写"。

我是怎么写的

这次我没有重写,而是在第二次作业的基础上加东西:

  • 写个Function类,存函数名、参数、函数体
  • 写个FunctionCallFactor类,表示调用函数
  • 写个DerivativeFactor类,表示dx()
  • 改Parser,让他能认识函数定义和dx()语法
  • 函数调用求导时,先把参数代进去,再求导

这次写起来比第二次顺手多了,可能是有经验了吧。

代码规模

第三次作业代码规模如下图:

(放SourceMonitor截图,总行数777行,13个类,63个方法)

代码777行,比第二次又多了50%。类有13个,新增的都是跟函数相关的。

类图

第三次作业类图(只看新增的):

屏幕截图 2026-05-18 122209

新增的这些东西:

  • FunctionManager是个单例(其实就是个静态Map),管理所有自定义函数
  • Function存函数的信息,call方法做参数替换
  • FunctionCallFactor表示调用函数,求导时先展开再求导
  • DerivativeFactor表示dx(),这个的求导规则我想了好久

复杂度分析

第三次作业的复杂度分析如下:

(放SourceMonitor的复杂度数据)

方法复杂度(挑几个高的):

类 方法 v(G) 说人话
Parser parseFactor 15 因子类型又多了函数调用和dx(),分支更多了
Parser parseFunctionDef 8 解析函数定义
Function call 10 参数替换,要遍历整个函数体
FunctionCallFactor derivative 8 函数调用求导,逻辑比较复杂

类复杂度:

类 OCavg WMC
Parser 6.2 62
Expression 3.8 27
Term 4.2 25
Function 3.5 14
其他类 ❤️ <10

从数据看:

  1. Parser类的总复杂度涨到62了,parseFactor的v(G)=15,因子类型越来越多,这个类越来越臃肿。

  2. Function.call方法的v(G)=10,参数替换要处理很多种情况,嵌套调用的时候还要递归替换。

  3. 各个基础Factor类还是保持低复杂度**,这说明接口设计得还行,加新功能不影响老代码。

Bug分析

公测

公测大部分过了,但有一个性能点没拿满。问题是函数嵌套调用时,我没有化简中间结果,导致输出很长。比如f(x)=x+x,调用f(sin(x)),我输出"sin(x)+sin(x)",但更简单的写法是"2*sin(x)"。

互测

互测被人发现了两个bug:

第一个:自定义函数递归调用自己会无限递归。比如f(x)=f(x)+1,我的程序一直调用自己,最后栈溢出了。题目应该是不让递归的,但我没做检查。

第二个:函数参数在函数体里没用上时会出错。比如f(x)=y(y是外面定义的变量),我的替换算法会把y也错误地替换掉。

互测中我hack了8个人,发现的bug:

  • 函数作用域搞不清楚,参数和外部变量混了
  • dx()嵌套求导算不对
  • 多参数函数解析有问题

测试方法

第三次作业继续升级测试:

  1. 写脚本随机生成函数定义和函数调用
  2. 专门测试嵌套调用和递归的边界情况
  3. 和几个同学互跑程序,发现不一致就互相通知,这个方法帮我找到了好几个自己没想到的bug

改进建议

第一次作业的改进

  1. 多用正则,少自己写判断。我那个checkFormat方法用了一堆charAt,换成正则表达式会简洁很多。

  2. 把输出单独写一个类。printPoly方法太复杂了,可以单独写个类负责输出化简。

  3. 早点想以后要加啥功能。第一次作业虽然简单,但如果稍微注意一下设计,第二次也不至于全重写。

第二次作业的改进

  1. parseFactor方法太长了,拆小点。可以拆成parsePowerFactor、parseTrigFactor这些小方法。

  2. 把化简单独弄个模块。toString里的化简逻辑和输出混在一起,分开会更好维护。

  3. 考虑用访问者模式。现在求导和输出都是Factor接口的方法,以后要加新操作就得改所有Factor类。

第三次作业的改进

  1. 加递归深度保护。防止无限递归导致栈溢出。

  2. 加个缓存。同一个函数用同样的参数调用多次的话,可以缓存结果,不用每次都重新算。

  3. 完善符号表。现在的函数是全局的,如果以后要支持函数里定义函数,就需要更复杂的符号表管理了。

总结

通过这三次作业,我学到了不少东西:

学到的:

  1. 面向对象设计。从第一次把所有代码塞Main里,到第三次有13个类、清晰的接口继承。虽然过程痛苦,但确实是进步了。
  2. 递归下降解析。学会了用递归的方式处理嵌套结构,对编译原理有了初步认识。
  3. 设计模式。实际用了接口、组合模式、单例模式,比光看书理解深多了。
  4. 自动化测试。搭了一个Python测试框架,能自动生成用例和验证结果,这个以后还能用。

还需要学的:

  1. 代码重构。知道代码有问题,但有时候不知道怎么改最好,需要多学学。
  2. 更多设计模式。访问者模式、解释器模式这些只是听说过,还没真正用过。
  3. 性能分析。这几次只关心对不对,没管性能,以后要学学。

对课程的建议:

  1. 希望不要讲的这么快,我们对这个编程没那么了解,上课快,只能靠自学,但是精力又有限,还有不懂得,作业难度和量也大。
posted @ 2026-05-18 12:27  杨杰七  阅读(4)  评论(0)    收藏  举报