301
社区成员
发帖
与我相关
我的任务
分享 由于生成法中可能造成的输入字符串中包含连续冗余的+-,空格,前导0等,采用建立processer类的方法对表达式进行预处理,方便后面的语义分析。考虑到后面性能要求中的长度问题,预处理阶段尽量的将字符串长度缩短是好事,我在处理时采用以下的顺序
\t)+-(其实就是为这个项确定出符号,或者说统计出负号的个数)+ 这样的顺序要优于2,3颠倒
Lexer主要进行词法分析,他负责从字符串中读到token并传递给Parser,这里我们的token可以用一个枚举类保存。
public enum TokenType {
NUM,X,MULTI,EXP,ADD,SUB,LP,RP,NULL
}
同样沿用training代码中的逻辑,可以用switch语句等保存token。
对于parser部分的设计同样沿用了training中的三个方法,根据当前lexer扫描到的token进行语义分析,在我的设计中参考了学长博客中的设计,为项设置出符号属性,在解析表达式时将项的符号传入。这里同样需要注意的是number^pow的形式并不符合形式化表述,我的第一版代码中对这种形式进行了解析
该类中借鉴了training中代码的三个方法进行解析
parseExprparseTermparseFactor parseFactor中需要注意的是,读到(,即读到表达式或读到x即读到幂函数,还要再向后读一个看是不是^,如果是则说明该因子有指数(要为表达式设置指数属性)
public Expr parseExpr() {
Expr expr = new Expr();
int sign = 1;//标记项的符号
if (lexer.getCurTokenType() == TokenType.SUB) {
//该项是负项
} else if (lexer.getCurTokenType() == TokenType.ADD) {
//该项为正项
}
expr.addTerm(parseTerm(sign));
while (//有下一项)
{
//正负项讨论
}
return expr;
}
// parseExpr将项的符号传入
public Term parseTerm(int sign) {
//...
while (//有下一个因子) {
//...
}
return term;
}
public Factor parseFactor() {
if (//如果是左括号,说明是表达式) {
//跳过LP
lexer.next();
Expr expr = parseExpr();
//跳过RP
lexer.next();
if (//如果是^) {
//
}
return expr;
} else if (//如果是数字) {
//...
} else if (//如果是减号,说明这是个负数) {
//...
} else { //如果读到x
//如果读到x则还需要判断他的下一个token是不是^
}
}
在经过语法分析之后,我们已经对表达式自顶向下建立起表达式树,而我们的输出结果,可以表示为多项式的形式
$$
Expr = \Sigma a_i x^{n_i}
$$
因此我们可以考虑在每一个语法层级都将其转化为多项式形式(构造toPoly方法),最终递归地得到计算结果。
为了实现计算,我们定义出单项式类Unit,多项式类Poly。每个单项式以数组的形式管理各个单项式。我们先不考虑各个类中具体的方法实现,先去大体认知这个与表达式树映射架构的多项式树。
我们的思路是将每一个层级都表现为多项式形式
numberFactor:对于数字因子,是树中的叶节点,我们可以简单的表示为只有一项的多项式:
$$
numberfactor = numberfactor*x^{0}
$$
这一项即为一个单项式,我们的多项式类中的数组中只保管这一个单项式即可
powFactor:对于幂函数因子,同样为树中的叶节点,我们可以简单的表示为只有一项的多项式:
$$
powfactor = 1*x^{powfactor}
$$
这一项即为一个单项式,我们的多项式类中的数组中只保管这一个单项式即可
Term:将term类转换为多项式形式,我们知道,多项式的表达形式为factor*factor*factor,我们只需要将他的各个因子的多项式相乘即可
$$
TermPoly = \Pi FactorPoly
$$
Expr:将expr类转换为多项式形式,表达式的形式为term + term + term (其中term中包含该项正负号),只需要将构成表达式的各个项的多项式相加即可
$$
ExprPoly = \Sigma TermPoly
$$
这样又构建起一个递归的过程,自底向上地构建起多项式树。
经过以上分析,我们知道需要新增的类有
Unit
Poly
需要新增的方法有
每一个层次的toPoly,包括Expr,Term,Factor
Poly中的计算方法:
addPolymultiPolypowPoly用于最终输出的Unit类和Poly类中的toString
Unit单项式类,其中包含两个属性ceo(系数),pow(指数),Poly类中包含ArrayList <Unit> unitlist 先从比较简单的底层实现,即numberFactor和powFactor
number toPoly
$$
number*x^0
$$
pow toPoly
$$
1*x^{pow}
$$
Term toPoly
public Poly toPoly() {
//...
for (Factor factor : factors) {
//各个因子多项式相乘
}
if (this.sign == -1) {
//多项式中单项式变号
}
return poly;
}
Expr toPoly
public Poly toPoly() {
//...
for (Term term : terms) {
//各个项多项式相加
}
return poly;
}
高层级调用低层级的toPoly,自底向上
计算思路:我们对两个多项式进行加法,关键的步骤在于合并同类项,我们已知多项式中以数组的数据结构保存着,我们先将两个数组合并为一个数组,之后在一个数组中进行合并同类项。
public Poly addPoly(Poly another) {
//将两个数组中的单项式合并到一个数组中
for(int i = 0; i < units.size(); i++) {
for(int j = i+1; j < units.size(); j++) {
//判断标准:指数相同,合并同类项
}
}
return new Poly(units);
}
在数组中进行遍历,若找到和i元素指数相同的j元素,则系数相加到i元素上,删除j元素,并将j回退一位,实际上是保证下次遍历从i+1开始。
计算思路:我们对两个多项式中各项相乘的结果加入新的多项式变量。这里注意:在我的实现方法中,是从底向上构建表达式,这样在相乘之前,项表达式是空的,需要进行判断,如果是空的,就返回当前因子表达式加入项表达式中,再继续与其他因子表达式相乘,当然还有另一种方法,就是在乘之前就先加进去一个因子表达式
public Poly multiPoly(Poly another) {
//...这里需要判断是不是空
if(如果为空){
//...
} else {
for(Unit unit1 : this.unitlist) {
for(Unit unit2 : another.unitlist) {
//因子多项式相乘
}
}
return new Poly(units);
}
}
对多项式中的每一个单项式的符号进行取反,我采用两步实现,在Poly中进行Unit遍历,在Unit类中利用BigInteger的方法.negate()取反。
我们在Unit类和Poly类中都要实现toString方法进行递归调用。
将每个unit转化为
$$
coe*x^{pow}
$$
将各个项多项式连接起来,并进行优化输出,常见的优化输出有:
系数为0:不输出
系数为1:省略系数,x
系数为-1:省略系数,-x
指数为0:输出1
指数为1:省略指数,x
之后可以调用之前字符串预处理过程中的几个方法
去掉连续的+-号
若第一项为负,则从后面找一个正项放在前面,大概给出这个方法的伪代码,这个方法的思路是:先判断第一项是否为负项(第一个符号是否为负号),之后看是否能找到正项,如果能就把他俩换一下位置,字符串层级的操作要注意substring方法包含左字符不包含右字符
if (this.input.charAt(0) == '-') {
int addpos = -1;
int i = 0;
for (i = 0;i < this.input.length();i++) {
//找到+号,记录位置
}
if (addpos != -1) { //如果找到了+,就找这一项后的符号,将这一项分割出来
for (i = addpos + 1;i < this.input.length();i++) {
//...
}
//处理字符串,将正项一道前面,之后会调用删除前导+的方法,使得表达式缩短一个字符,这么麻烦其实只缩短了一个字符hhhhhh
}
}
去掉不必要的前导+
同时要注意如果输出长度为0,要补充输出“0”
在第一版代码中,构建多项式过程中我们使用arraylist来存储单项式,之后在后续计算过程中的时间复杂度:
这样的时间复杂度在面对一些指数比较大的表达式时会出现TLE的情况,后来经过同学的提示,可以使用HashMap来存储单项式,key为指数,value为系数,这样的确可以大大简化时间复杂度,同时也可以省去一个类Unit(~~但是我没舍得删)。另外一个点:可以在每次乘法结束之后都进行一次合并同类项addPoly,这样可以更加减少时间复杂度
public Poly addPoly(Poly another) {
Iterator<Map.Entry<Integer, BigInteger>> iterator = this.unitmap.entrySet().iterator();
while (((Iterator<?>) iterator).hasNext()) {
Map.Entry<Integer,BigInteger> entry = iterator.next();
int pow = entry.getKey();
BigInteger coe = entry.getValue();
if (another.unitmap.containsKey(pow)) {
BigInteger coe1 = another.unitmap.get(pow).add(coe);
another.unitmap.put(pow,coe1);
} else {
another.unitmap.put(pow,coe);
}
iterator.remove();
}
return another;
}
主要思路是数学上的多项式合并思路,遍历一个多项式,在另一个多项式中检查是否有这个项的指数,如果有就合并,如果没有就加入到另一个多项式,最终返回另一个多项式
这里需要注意的是,关于HashMap边遍历边删除的需求,如果使用增强for循环,会报错。需要使用迭代器进行。
Iterator<...> iterator = ...iterator();
while (iterator.hasNext()) {
//...
iterator.remove();
}
public Poly multiPoly(Poly another) {
if (this.unitmap.isEmpty()) {
return new Poly(another.unitmap);
} else {
HashMap<Integer, BigInteger> emptyMap = new HashMap<>();
Poly emptypoly = new Poly(emptyMap);
HashMap<Integer, BigInteger> hashMap = new HashMap<>();
for (int pow1 : this.unitmap.keySet()) {
for (int pow2 : another.unitmap.keySet()) {
int pow = pow1 + pow2;
BigInteger coe = this.unitmap.get(pow1).multiply(another.unitmap.get(pow2));
if (hashMap.containsKey(pow)) {
BigInteger coe1 = hashMap.get(pow).add(coe);
hashMap.put(pow,coe1);
} else {
hashMap.put(pow,coe);
}
}
}
return new Poly(hashMap);
}
}
这里不涉及到对于元素的删除等操作,可以使用增强for循环
这里需要明确java中HashMap的一个特性:当我们向HashMap中加入已经存在的key时,会覆盖掉原来的value,我利用这一点进行了value的更新
there is a new <newkey,newvalue>
if (newkey exists) {
find(key,value)
value -> newvalue
} else {
put<newkey,newvalue>
}
同样要注意对于存在的key的value的值的更新
public void negate() {
for (Integer pow : this.unitmap.keySet()) {
BigInteger coe = this.unitmap.get(pow);
this.unitmap.put(pow,coe.negate());
}
}

我使用Metricsreloaded进行分析,先对几个标准进行解释
Cogc:圈复杂度,程序中独立路径的数目ev(G):本质复杂度,程序中必须要有的控制流程数目iv(G):内在复杂度,程序本质上的复杂度v(G):程序体积,程序中独立语句数目 以下为程序中复杂度超标的方法

可以看到复杂度主要集中在进行化简输出的部分或对输入字符串进行预处理的方法,字符串预处理方法需要对字符串进行循环遍历并在过程中条件判断,化简输出部分有对每一项指数、系数的讨论,复杂度均较高,其中语义解析部分parseFactor由于有多种因子,复杂度较高。
在第一次作业中强测以及互测均未出现bug,在互测环节中房友的优化输出出现bug,hack数据为1+(x^8)^8
,后不必要的+ 本次作业中,新增了EXP,F,G,H,COMMA(逗号)等token,需要增加识别功能。
在Parser类中新建一个方法parseExp,当我们读到的token为EXP时调用该方法进行解析。首先对括号内的因子进行解析,再对括号外的指数进行解析。
$$
exp()^n \space | \space exp()
$$
public Factor parseExp() { // exp(<factor>)^<num> | exp(<factor>)
// lexer : exp -> pos = pos + 4 already into the (
// 读到RP时接着往后读看有没有指数
int pow = 1;//默认指数为1
Factor innerFactor = parseFactor();
if (lexer.getCurTokenType() == TokenType.POW) {
// 后面有指数
}
return new Exp(innerFactor, pow);
}
为了成功地解析自定义函数,我们首先定义出Definer类,他的主要作用是处理自定义函数的定义以及调用。(相当于parseFunc的slave)其中定义出两个HashMap
private static HashMap<String, String> funcMap = new HashMap<>();
private static HashMap<String, ArrayList<String>> paraMap = new HashMap<>();
funcMap构建了以函数名[f|g|h]为key,函数定义式为value的HashMap
paraMap构建了以函数名[f|g|h]为key,函数形参formalParas为value的HashMap
注意:这里由于存在定义式中形参x与指数符号exp重复的问题,可以考虑在addFunc过程中对x,y,z替换为w,q,r,但是这里处理也同样需要注意不要替换掉exp中的x,在callFunc中直接对w,q,r进行替换,在周三早上的研讨课听到,感觉比较优雅,原来的实现是在callFunc时判断逻辑不要让exp中x被替换。复杂度上相同,但更倾向换参版本。
其中定义出两个方法,一个是addFunc用于处理输入,在输入时将字符串传入addFunc方法,构建函数的定义式到funcMap和paraMap中,另一个是callFunc用于输入解析parser,将函数名和实参列表传入,依据之前构建好的funcMap和paraMap得到用实参替换形参之后的表达式字符串。
public static void addFunc(String input){ // 输入的函数定义式
//首先进行形参替换
//主要调用 String.split("...")方法对函数的定义式进行分割,并分别put到funcMap,paraMap
}
public static String callFunc(String funcName, ArrayList<Factor> actualParas) {
// 自定义函数解析时使用 获得替换后的函数表达式
// 获得这个函数名对应的定义式以及形参列表
for (//遍历形参列表) {
//将函数定义式中的形参用实参替换 替换时一定要注意在两边加括号!!!!!
}
return funcDef;
}
注:这里传入的是Factor,可以调用toString方法进行转化,在接口中定义方法toString,在各个implement class中Override,在这里我们实现的确确实实就是把形参替换成实参,只需要考虑字符串层面的操作,怎么把这一项用字符串描述出来.
number:直接将数字转化为字符串
return number.toString();
pow:较为简单的拼接字符串
return "x^" + pow;
Func类:自然而然的返回属性newFunc.
return this.newFunc
Exp类:由于指数exp(<factor>)^n的形式,递归调用Factor.toString方法。
public String toString() { // exp(factor)^n toString
if (pow == 1) {
return "exp(" + factor.toString() + ")";
} else {
return "exp(" + factor.toString() + ")^" + pow;
}
}
Expr类:表达式类转换为字符串,递归调用Term.toString,记得后面判断加上指数,而且左右一定要有括号
public String toString() {
StringBuilder sb = new StringBuilder();
for (Term term : terms) {
//递归 Term.toString
}
//加上指数
return sb.toString();
}
Term类:需要注意的是项可能有符号
public String toString() {
StringBuilder sb = new StringBuilder();
if (this.sign == -1) {
sb.append('-');
}
for (Factor factor : factors) {
//递归调用 factor.toString
}
return sb.toString();
}
需要注意的是,这个类中的属性和方法都被定义为了static属性,即这些属性和方法都是类所有的,而不是对象所有的,对于方法而言可以直接通过类进行调用。
接下来使用Definer帮助解析parseFunc.其中函数调用的形式为
$$
f(,,)
$$
解析逻辑为先解析函数名,然后解析所有的实参。
关于Func类
在Func类中设置两个属性
private String newFunc; //实参带入形参之后的结果
private Expr expr; //newFunc解析成表达式后的结果
在Func类的构造方法中,我们传入函数名[f|g|h]和实参列表actualParams,通过调用Definer类中的静态方法callFunc得到替换为实参之后的表达式,然后对表达式进行解析。
public Func(String name, ArrayList<Factor> actualParams) {
this.newFunc = Definer.callFunc(name,actualParams); //形参替换为实参->字符串
this.expr = this.setExpr(); // 解析成表达式
}
private Expr setExpr() {
//newFunc字符串预处理
//new lexer
//new parser
return parser.parseExpr(); //字符串解析为表达式
}
关于parseFunc
负责对自定义函数中的实参部分进行解析,和函数名一同传给Func再newFunc->setExpr得到解析好的表达式。
public Factor parseFunc(String name) {
//函数调用的形式为 f(<factor>,<factor>,<factor>) 进来时正读到f
//读f-LP
//跳过f-LP
ArrayList<Factor> actualParams = new ArrayList<>();
//将第一个实参parseFactor并加入list
while (//后边还有因子) {
//继续加入
}
//跳过f-RP
return new Func(name, actualParams);
}
继续沿用第一次作业中Unit和Poly的思路,这次化简结束之后的表达式形式应当为
$$
Expr \space = \space \Sigma (ax^n * \Pi exp()^{n_i})
$$
即每一个Unit中的内容应当为
$$
Unit \space = \space ax^n * \Pi exp()^{n_i}
$$
考虑到exp的形式可以进行化简,但我们先不化简,在Unit类中新增一个用于表示指数函数的HashMap。
HashMap<Poly,Integer> expMap;
key为factor的多项式value为exp的指数(expMap这里不同键值对之间实际上表示的是乘法关系,这样两个Unit相乘时,只需要将两个expMap合并在一起就模拟了相乘的效果)
在Poly类中新增一个存储Unit的HashMap
HashMap<Unit,BigInteger> unitMap
这里需要注意的是因为key为自定义类,需要对hashCode(),equals()方法进行重写
equals()
equals方法用来比较两个引用对象是否为同一个对象,在超类Objects类中的定义为比较两个引用指向的对象是否相等。
public boolean equals(Object obj) {
return (this == obj);
}
而在我们的自定义类中,我们定义出来很多属性,在我们的作业内容中,以Poly中定义的单项式哈希表为例,
HashMap<Unit,BigInteger> unitMap
Unit的属性有coe,pow,expMap。那么两个Unit之间着三个属性相同,我们就认为他们符合equals方法,判断得到应当为相同对象。因此我们应当在Unit类中覆写equals方法,加入判断这几个属性如果都相等就是相同对象的判断。为了避免空指针,使用Objects.equals进行判断。
public boolean equals(Object obj) {
if (obj instanceof Unit) {
Unit unit = (Unit) obj;
return Objects.equals(coe,unit.coe) && Objects.equals(pow,unit.pow) && Objects.equals(expMap,unit.expMap);
} else {
return false;
}
}
hashCode()
正如方法的名称,hashCode()返回值为对象的哈希编码,即在table中的索引,用来确定对象的存储地址。在自定义类的超类 Objects宏对于hashCode()的描述主要可以概括为以下几点
hashCode(),要求返回值相等equals方法,两个对象的hashCode相同对于我们的自定义类,我们可以通过类中的属性来计算hashCode,这样可以达到满足equals即返回相同hashCode的效果。(对于equals判断中使用的属性,计算hashCode时一定要用到,没使用的属性计算时一定不要用)。为了避免空指针,我们常调用Objects.hash(para1,para2,...)的方法进行计算。
public int hashCode() {
return Objects.hash(coe,pow,expMap);
}
题目要求中对于必要的括号定义如下
exp调用的括号exp() 当前我的实现中,判断是否加括号的逻辑在exp(<poly>)中对poly调用toString方法时进行。此时构建的多项式结构为

exp()内一定为因子,不需要加括号的因子有常数因子,幂函数,指数函数,需要加括号的因子为表达式因子。我写了返回boolean类型的addPar方法,分别对exp内的poly分情况进行判断,这里加不加括号与Unit中toString方法的优化有关,也就是说若这个Unit是常数时,返回的字符串一定是常数,而不是3*x^0之类,只要我们对每个Unit的化简足够可靠(最简),就可以保证这里的逻辑只需要对coe,pow,expMap中有效项个数等量进行简单判断。这里主要给出判断伪代码。
public static boolean addPar(Poly poly) {
if (//若多项式中有两项及以上) {
return true;
} else if (//若多项式中只有一项) {
if (//系数为0) {
return false;
} else {
if (//是数字) {
return false;
} else if (//是幂函数) {
return false;
} else if (//是指数函数) {
return false;
}
}
}
return true;
}
注:Simplify为定义出的用于化简的静态类,其中getValidExp用于查找exp哈希表中“有效项”,存储时形式为HashMap<Poly,Biginteger>,所谓有效项就是指数大于0
对于优化输出,我的思路是在构建字符串时进行化简,而不是对字符串进行化简,在这次作业中我进行的优化有
exp(0)=1 一定要慎重优化,避免出现bug!
对于优化,我单独建立了Simplify类,其中定义出多个静态方法用于表达式化简,这里一定要与addPar中的逻辑对应,即相应的数字,幂函数,指数函数都要化简为最简的形式。
在进行优化输出的过程中,涉及到对Unit,Poly等类的深克隆,为了避免对这些类中的对象进行递归克隆,我采用了序列化克隆的方法(有模版比较省事),只需要把Unit以及Poly接到Serializable接口上并重写cloneSerializable方法即可。给出克隆模版
public Unit cloneSerializable() {
Unit unit = null;
try {
//序列化对象
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//反序列化对象
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
unit = (Unit) ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return unit;
}
我认为这个方法是极为方便的,唯一的缺点就是会导入一堆java.io包,IDEA可能会自动帮你合并为java.io.*,但这是不符合代码规范的,需要手动调整。


爆复杂度部分和上次作业一样集中在输入预处理以及输出化简部分。
Integer.parseInteger()被爆范围 在上一次的作业中我们使用了字符串替换的方法用来解析函数定义式和调用,这种架构在这一次作业中自动满足了函数中可以调用先前已经定义函数的要求。
对于求导因子的处理是新建一个求导因子类,其中设置属性保存需要求导的多项式。求导的过程在建立多项式过程中进行,即在表达式转化为poly的过程中调用derive进行求导,递归地转化为对每一个unit求导,只需要对unit写一个求导方法即可。或者说求导因子类的toPoly返回的poly是求导之后的poly,toString方法返回的是求导之后的多项式字符串(两边一定记得加括号)

这次作业的代码在上次代码的基础上实际上只增加了求导环节,代码复杂度上变化不是很大,主要是输入预处理以及输出化简环节复杂度较高。
经过第一单元的学习,在三次迭代作业中我更加感受到迭代开发中架构的重要性,由于在第一次作业时借鉴了学长博客中的架构,在后续的开发中我并没有进行remake。第一单元中主要学习的层次化设计思想在作业中有很好的体现,递归下降算法的思路初次接触可能有些陌生,但是上手开始写代码就会有更加清晰的体会。第一单元作业的顺利完成离不开秋季学期pre课程的预热,作业梯度的设置也在逐步完善,总体来说体验不错。