2022面向对象第一单元总结
第一次作业
核心类图
架构分析
- 主体分为解析、化简、合并三个过程,三者之间基本解耦
- 优化在合并过程中完成
- 支持括号嵌套
解析
- 核心类:
Parser
,Lexer
- 采用递归下降法解析表达式,后续两次作业在此基础上进行迭代
Lexer
处理输入字符串,一次可取出一个整数或一个其他字符Parser
依据表达式树的结构进行解析,生成表达式树之后不再对字符串进行操作
化简
- 核心类:
Expr
,Term
,Power
,Factor
,Var
- 核心类均为不可变类,化简时会另外生成一个新的对象,不改变原有对象结构
- 自下往上化简,减少表达式树深度,最终得到嵌套层度为 0 的表达式(未合并同类项),例如
1+1*x+1*x+1*x**2
- 表达式树的结构与官方的形式化表述在因子层级不同,笔者将幂函数
Power
作为项Term
的组成部分,变量Var
和表达式Expr
实现接口Factor
并作为幂函数的底数 - 与官方形式化表述的对应关系:
( Var ) ** 0
<==> 常数因子( Var ) ** <非0指数>
<==> 幂函数因子( Expr ) ** <指数>
<==> 表达式因子
合并
- 核心类:
Poly
,Power
- 利用
HashMap<BigInteger, BigInteger>
进行合并同类项,key
为幂函数的指数,value
为幂函数的系数
优化
-
系数优化
1*x => x
-1*x => -x
-
指数优化
x**0 => 1
x**1 => x
x**2 => x*x
-
正项提前
-1+x => x-1
基于度量的分析
代码规模分析
- 总行数小于 500 行,代码规模不大
- 类行数均小于 100 行,功能相对分散
方法复杂度分析
- 平均复杂度不高,方法间耦合度低
- 复杂的较大的方法主要是项的化简以及表达式的字符串化函数
类复杂度分析
- 平均循环复杂度不高,
Poly
在合并同类项以及输出优化时进行过多次循环,因此复杂度相对较高
测试数据
随机数据
- 生成方法:表达式逆解析, 以随机表达式为例:
-
范围限制
- 最大项数
- 最大因子数
- 括号嵌套数:官方测试数据不允许括号嵌套,但在本地测试时可以允许
- 最大指数:不超过 8
exp( expr ) = max{ exp( term ) }
exp( term ) = sum{ exp( power ) }
常量池数据
- 在各个层级将随机生成改为从常量池中选取
- 介于随机数据和特殊数据之间,属于半随机数据
特殊数据
- 整数溢出:
(9999999999999999999999999)**8
- 指数上限:
(x+x+x+x+x+x+x+x)**8
- 全 0 数据:
0+0*(0*0*0+0*0*0)*(0)**0+0*0*(0)**0
第二次作业
核心类图
架构分析
- 主体分为解析、化简、合并三个过程,三者之间基本解耦
- 优化在化简和合并过程中完成
- 支持因子嵌套和函数嵌套调用,可直接作为第三次作业
解析
- 核心类:
Parser
,Lexer
Lexer
新增nextIdentifier
方法,一次读出一个标识符,用于获取变量、函数名称Parser
根据此次作业表达式树的结构进行了部分重构与迭代
化简
- 核心类:
Expr
,Term
,Factor
,Constant
,Power
,ExprFactor
,Triangle
,Sum
,FuncCall
,FuncDef
,Var
- 相较于上次作业,在因子层级进行了重构,与官方形式化表述完全一致
Factor
接口simplify
方法:返回类型不一定与原类型相同,例如Power
类对象x**0
在化简后可以返回Constant
类对象1
substitute
方法- 用于进行函数参数带入,将
Var
对象替换成Factor
对象 - 只有在
Power
层级才进行带入,其他层级则继续向下传递或停止
- 用于进行函数参数带入,将
合并
- 核心类:
Poly
,Factor
- 重构
Poly
类,利用HashMap<Factor, BigInteger>
存储项,HashMap<HashMap<Factor, BigInteger>, BigInteger>
存储表达式 x*x*sin(2) + 3*cos(1) => HashMap{ <HashMap{ <x, 2>, <sin(2), 1> }, 1>, <HashMap{ <cos(1), 1> }, 3> }
优化
-
相较于上次作业,新增三角函数相关的各种优化(因为数据限制,部分优化到第三次作业才起作用)
-
符号优化
sin((-x)) => -sin(x)
cos(-1) => cos(1)
-
括号嵌套优化
sin((2)) => sin(2)
sin((1*x*x)) => sin(x**2)
-
公式优化
cos(x)**2 + sin(x)**2 => 1
1 - sin(x)**2 => cos(x)**2
2*sin(1)*cos(1) => sin(2)
基于度量的分析
代码规模分析
Poly
类和Triangle
类行数较多,因为优化主要在这两个类里完成,所需代码量较大
方法复杂度分析
- 平均复杂度不高,耦合度较低
- 复杂度较大的几个方法均是用于优化的,条件判断与循环结构较多
类复杂度分析
Poly
与Triangle
包含大量优化方法,循环结构较多,复杂度较高
数据生成
随机数据
- 进行了简单迭代,增加了新出现的一些因子,新增了规则限制
- 规则限制
- 可使用的变量
- 自定义函数定义:x, y, z
- 主表达式:x
- 求和函数:已知的可使用变量 + i
- 禁止函数调用
- 自定义函数定义和求和函数
- 自定义函数调用(第三次作业允许)
- 可使用的变量
常量池数据
- 新增三角函数池
特殊数据
- 针对本次作业规则以及自己的优化过程,新增了一些手动构造的数据
- 括号嵌套:
sin(((f(g(f(h(-1))))))), 其中f(x) = g(x) = h(x) = (x)
- 三角化简:
- 符号化简:
sin(-1)**2, cos(-1)**2, sin((-x-1))
- sin2 + cos2 = 1:
cos(x)**2+sin(x)**4+cos(x)**2*sin(x)**2
- 1 - sin2 = cos2:
sin(x)**2*cos(x)**2-sin(x)**2
- sin 二倍角公式:
-3*2*sin(1)**2*cos(1)**2
- 符号化简:
第三次作业
核心类图
架构分析
- 主体分为解析、化简、合并三个过程,三者之间基本解耦
- 优化在化简和合并过程中完成
解析
- 较上次作业没有变化
化简
- 较上次作业没有变化
合并
- 核心类:
Poly
,Item
,Factor
Item
类- 继承自
HashMap<Factor, BigInteger>
类 - 主要目的是为了缩短类名,并增加两个方法用于三角函数优化,使用更加方便
- 继承自
优化
- 新增公式优化:
1 - 2*sin(1)**2 => cos(2)
- 公式优化策略
- 贪心算法
- 利用优先队列
PriorityQueue<Poly>
,按字符串长度排序 - 每一轮尝试所有可能的公式变形,生成新的
Poly
对象扔进优先队列。结束后取出队首元素,若该表达式长度小于原本表达式,则进行替换,并继续化简;否则结束化简。
基于度量的分析
代码规模分析
- 由于第二次作业已满足本次作业要求,所以并未有太大改动,代码行数基本不变
方法复杂度分析
- 对优化方法做了一些改动和优化,所以复杂度略有下降
- 复杂度较大的方法仍集中在优化部分
类复杂度分析
- 引入的
Item
类分担了原本Poly
类的部分功能,使Poly
复杂度有所下降 - 各项平均复杂度也均有降低
数据生成
随机数据
- 较上次作业没有太大改动
常量池数据
- 较上次作业没有太大改动
特殊数据
- 针对新增的优化方法以及并未实现的优化方法构造了一些数据
- cos 二倍角公式:
2*cos(x)**2*cos((2*x))+sin((2*x))**2+cos((2*x))
- 和差角公式:
2*cos(x)**2*cos((2*x))+sin((2*x))**2+cos((2*x))
测试策略与bug分析
测试策略
基本流程
- 主要采用黑盒测试 + 随机轰炸
- 自动测试:命令行编译、运行,使用
subprocess
模块 - 正确性检查:利用
sympy
库 - 格式检查:利用正则表达式
对拍测试
- 本地三人对拍,用于检测自己程序的正确性
- 可同时检测三个人的 bug,并且三个人同时跑的话效率能高 3 倍
多人测试
- 用于互测,以自己的程序为标准与其他人对拍
- 利用循环结构进行串行测试
数据分析
- 通过数据拆解或答案比对,构造出引起错误的极小项
- 随后调整数据生成器或者修复他人bug
- 避免不断 hack 同质 bug,增加 hack 效率
本人bug
- 三次强测和互测中均未发现 bug
- 本地测试中遇到过的一些问题
- 参数代入问题
- 数据:
0 f(y, x) = x*sin(y) f(x, 1)
- 原因:原本
substitude
方法的参数表是(Var, Factor)
,一次只能代入一个参数。因此会先代入y=x
,得到x*sin(x)
,再代入x=1
,得到sin(1)
,与答案sin(x)
不符 - 解决办法:修改参数表为
(HashMap<Var, Factor>)
,同时传入所有要代入的参数。并且能提升效率(只遍历一遍表达式树,原本可能遍历 1 ~ 3 遍)
- 数据:
- 优化后变长
- 数据:
-3*2*sin(x)**2*cos(x)**2
- 原因:不进行二倍角优化,结果是
-6*sin(x)**2*cos(x)**2
;进行二倍角优化,结果是-3*sin((2*x))*sin(x)*cos(x)
,答案更长 - 解决办法:引入优先队列
PriorityQueue
来存储优化的结果,并与原表达式进行长度比较
- 数据:
- 参数代入问题
他人bug
- 第一次作业:hack 1 人 1 个 bug
- 第二次作业:hack 4 人 8 个 bug
- 第三次作业:hack 3 人 6 个 bug
- 完全采用黑盒测试,并未根据他人代码构造针对性样例
- 根据公开的hack结果,只有第三次作业的一个点并未hack到,原因是没有进行严格的格式检查;总体来说测试数据的强度还可以,覆盖率较高,并没有出现因数据问题而没有hack到的情况
反思总结
迭代与重构
- 第一次作业到第二次作业迭代的工作量较大,主要原因有三
- 在因子层级进行了重构
- 新增了较多因子,代码量翻倍
- 做三角函数优化花了不少时间
- 第二次作业到第三次作业几乎没有进行什么修改
- 第二次作业已经能满足第三次的要求
- 增加了一点三角函数优化
- 修改了优化部分的代码,复杂度更低
架构的优缺点
优点
- 代码逻辑清晰,不同功能类之间的耦合度较低
- 对于表达式树的维护较好,具有较强的鲁棒性、可扩展性
缺点
- 优化部分算法简单,效率较低,很多情况无法得到最优解
- 在表达式树的设计中没有引入运算符层级,如果新增除法或其他运算符,可能会需要不小的重构
心得体会
- 提升了面向对象思维能力和层次化设计能力
- 学会了递归下降法解析表达式
- 对于设计模式有了一定了解
- 数据构造和自动化测试水平提高