前言
第一单元的主要内容为输入表达式并对其化简,对其中的一部分进行求导,通过四次讲师授课、两次上机实验、两次研讨课,完成三次迭代作业。
培养面向对象编程的思维方式,培养层次化和模块化设计的能力。
第一次作业
第一次作业的主要内容是对输入的表达式进行化简、去括号。形式化表述如下:
表达式 → 空白项 [加减 空白项] 项 空白项 | 表达式 加减 空白项 项 空白项
项 → [加减 空白项] 因子 | 项 空白项 '*' 空白项 因子
因子 → 变量因子 | 常数因子 | 表达式因子
变量因子 → 幂函数
常数因子 → 带符号的整数
表达式因子 → '(' 表达式 ')' [空白项 指数]
幂函数 → ('x' | 'y' | 'z') [空白项 指数]
指数 → '**' 空白项 ['+'] 允许前导零的整数 (注:指数一定不是负数)
带符号的整数 → [加减] 允许前导零的整数
允许前导零的整数 → ('0'|'1'|'2'|…|'9'){'0'|'1'|'2'|…|'9'}
空白项 → {空白字符}
空白字符 → (空格) | \t
加减 → '+' | '-'
分析
由形式化表述可以看出,一个表达式(expr)可以是多个项(term)的和,一个项(term)可以是多个因子(factor)的乘积,factor又分为三种类型。
所以在构造时,在Expr类里设计了Arraylist<Term>的数组来存放项,在Term里设计了Arraylist<Factor>来存放因子。
用递归下降的方法解析字符串,然后进行化简、合并同类项等。
笔者选择在lexer中进行空格和\t的处理,在parser中进行连续正负号的处理解析,其实可以把这些算作预处理的部分放在导入lexer之前。
笔者在Term类里增加termSimplify方法,主要处理项内部的因子相乘(表达式相乘),如(x+1)*(y+3),然后返回一个Expr。在Expr类里加入termAdd方法,对每个Term进行termSimplify,然后对新得到的表达式进行合并同类项:新建HashMap<String, BigInteger>,把每一项中xyz的指数拼接成为一个字符串,作为Hashmap的key,该项的常系数作为value,把得到的表达式的每一项加入到hashmap中,再对hashmap遍历输出。
UML类图
复杂度分析
可以看到Expr类里的termAdd(化简所有单项式)方法和print方法四处爆红,Simplify(合并同类项)复杂度也较高。此外,Parser中的parseExpr方法和parseTerm方法复杂度也较高,主要因为if-else判断较多。
第一次作业代码量不是特别大,笔者控制在500行内(互测房间甚至看见300-的房友),expr类行数较多,爆红的地方也在expr类里。
bug分析
在强测和互测中,笔者被发现了一个bug:指数前的+号没有解析 (弱测中测真的弱) 。
互测里发现了别人的一些bug。有些同学应该是在输出前用字符串匹配直接删除1*达到化简效果,但是遇到11*x这样的输入会直接输出1x出现bug。 化简有方法,实现需谨慎
第一次作业总结
写完了第一单元之后,再回头看第一次作业,其实难度不是特别大,但是毕竟是初见,所以一直到周五都不知道从何处下手,周末几乎是卡着ddl交上去的作业。开始一定要思考好架构,而且要趁早动笔,不然到最后会很慌张。
对我来说难处主要在初次写递归下降时无从下手,没有全面理解面向对象的思想,写起来很吃力。
第二次作业
第二次作业在第一次的基础上增加了自定义函数、三角函数,且要求支持括号嵌套。形式化表述如下:
表达式 → 空白项 [加减 空白项] 项 空白项 | 表达式 加减 空白项 项 空白项
项 → [加减 空白项] 因子 | 项 空白项 '*' 空白项 因子
因子 → 变量因子 | 常数因子 | 表达式因子
变量因子 → 幂函数 | 三角函数 | 自定义函数调用
常数因子 → 带符号的整数
表达式因子 → '(' 表达式 ')' [空白项 指数]
幂函数 → 自变量 [空白项 指数]
自变量 → 'x' | 'y' | 'z'
三角函数 → 'sin' 空白项 '(' 空白项 因子 空白项 ')' [空白项 指数] | 'cos' 空白项 '(' 空白项 因子 空白项 ')' [空白项 指数]
指数 → '**' 空白项 ['+'] 允许前导零的整数 (注:指数一定不是负数)
带符号的整数 → [加减] 允许前导零的整数
允许前导零的整数 → ('0'|'1'|'2'|…|'9'){'0'|'1'|'2'|…|'9'}
空白项 → {空白字符}
空白字符 → (空格) | \t
加减 → '+' | '-'
分析
第二次作业出现了自定义函数,笔者最初的想法是表达式替换,但是没有写明白而放弃,在同学提示下转为字符串替换。
自定义函数也可以作为一种预处理,在分析表达式之前就现将表达式里出现的自定义函数替换掉,得到不含自定义函数的表达式,再进行分析。
三角函数部分,笔者新建了Sine和Cosine类,同属于因子。
在本次作业里,笔者将lexer中对空格和\t的简化放到了MainClass里进行预处理。
本次作业里笔者放弃了第一次作业中的HashMap,即放弃了后面的合并同类项过程。在Expr类的termAdd中,每一个项用Arraylist存储其中的因子,并安排好顺序。
term.addFactor(num);
term.addFactor(new Variable("x", flag1));
term.addFactor(new Variable("y", flag2));
term.addFactor(new Variable("z", flag3));
term.addAllFactor(triangleFactor);
其中flag1~flag3为xyz的指数,triangleFactor为存放三角函数因子的数组。
第二次作业在parser类和ExprPow进行了一些修改,使得解析到表达式因子的指数不为1时,返回(表达式因子)(表达式因子)……的形式,返回值类型为Expr。
public Expr exprPowSimplify() { //ExprPow类中
Expr expr = new Expr();
Term term = new Term();
for (int i = 0; i < exponent; i++) {
term.addFactor(this.getBase());
}
expr.addTerm(term);
return expr;
}
public Factor parseExprFactor() { //Parser类中
Expr expr = parseExpr();
lexer.next();
if (lexer.peek().equals("**")) {
lexer.next();
if (lexer.peek().equals("+")) {
lexer.next();
}
int exponent = Integer.parseInt(lexer.peek());
if (lexer.peek().equals("0")) {
lexer.next();
return new Number(BigInteger.valueOf(1));
} else if (lexer.peek().equals("1")) {
lexer.next();
return expr;
} else {
lexer.next();
ExprPow exprPow = new ExprPow(expr, exponent);
return exprPow.exprPowSimplify(); //此处进行优化
}
} else {
return expr;
}
}
第二次作业里笔者改变了输出方式,将输出行为同样下降到每个类里重写toString方法。
UML类图
复杂度分析
第二次作业爆红的方法开始多了起来,很多bug也由此而生。Cosine和Sine类的toString爆红,主要因为cosine和sine的内部因子会是很多情况(Expr,Term,Variable等等),因此使用很多if-else来判断。
Term类的termSimplify爆红,同样是因为factor的种类增多,分类讨论的情况也更多。
Expr类里的termAdd爆红是第一次的历史遗留问题)
由于笔者使用字符串替换处理自定义函数,而且Functions类的设计有点面向过程编程的嫌疑(写着写着就感觉自己在写C语言程设的题),replaceFunction方法里也用了很多if-else,爆红也是意料之中,或许可以优化一下。
第二次的代码量控制在了1000行内,parser、expr、functions行数略多,爆红的地方也基本上在这三个类里面。expr类没有第一次多是因为笔者放弃了合并同类项
bug分析
在强测和互测中出现了很多bug,都是由于写toString输出方法时没有考虑到某些情况,比如忽略了项的系数为0的情况导致没有输出。
互测未发现其他人的bug。
第二次作业总结
第二次作业写的也很痛苦,主要是在处理自定义函数方面犹豫了好长时间,表达式替换越写bug越多,直到周六晚上才选择字符串替换。
第一次作业里可以直接实现括号嵌套,如果设计得好的话会更方便一些。好的架构真的很重要
第三次作业
第三次作业在第二次作业的基础上增加了自定义函数定义时的互相调用(f(x) = 0, g(x) = f(x) + 1)和求导部分。形式化表述如下:
表达式 → 空白项 [加减 空白项] 项 空白项 | 表达式 加减 空白项 项 空白项
项 → [加减 空白项] 因子 | 项 空白项 '*' 空白项 因子
因子 → 变量因子 | 常数因子 | 表达式因子|求导因子
变量因子 → 幂函数 | 三角函数 | 自定义函数调用
常数因子 → 带符号的整数
表达式因子 → '(' 表达式 ')' [空白项 指数]
幂函数 → 自变量 [空白项 指数]
自变量 → 'x' | 'y' | 'z'
三角函数 → 'sin' 空白项 '(' 空白项 因子 空白项 ')' [空白项 指数] | 'cos' 空白项 '(' 空白项 因子 空白项 ')' [空白项 指数]
指数 → '**' 空白项 ['+'] 允许前导零的整数 (注:指数一定不是负数)
带符号的整数 → [加减] 允许前导零的整数
允许前导零的整数 → ('0'|'1'|'2'|…|'9'){'0'|'1'|'2'|…|'9'}
空白项 → {空白字符}
空白字符 → (空格) | \t
加减 → '+' | '-'
分析
第三次作业新增的求导,看上去很恐怖(笔者在周二上课知道第三次作业是求导的时候直接裂开来),但是实现过程没有非常难,在每个相关类里都新增求导方法即可。周五上机的代码也提供了很大部分的思路,包括深克隆、求导过程等。
对于新增的函数嵌套,笔者延续了预处理的思路:在每个函数定义时就对后面的表达式进行解析,如果调用了之前的函数,就直接替换掉,然后再返回字符串形式(toString)的自定义函数表达式,得到的函数表达式就没有其他自定义函数了。
对于求导,笔者新增了求导因子,如果解析到求导因子(dx,dy,dz)则对后面的表达式进行求导,返回Factor。 为后面的bug埋下了伏笔
UML类图
复杂度分析
第三次作业爆红的方法同样集中在第二次作业的几个地方上,而且随着三角因子、求导因子的加入,parser也开始逐渐爆红。
由于上机提供了非常好的思路(以及代码),第三次作业新增代码量不多。
bug分析
在强测和互测中出现了很多bug(三次作业都有bug的屑笔者),主要是在求导部分出现。
笔者采用了上机时的代码,但是没有认真阅读、仔细分析。
public Factor derive(String partialName) { //Expr类里的求导方法
Expr expr = new Expr();
for (int i = 0; i < terms.size(); i++) {
expr = Expr.mergeExpr(expr, (Expr) terms.get(i).derive(partialName));
}
Term term = new Term();
term.addFactor(expr);
return term;
}
public Factor PartialSimplify(String partialName) { //解析到求导因子后的处理,在Partial类里
Expr expr = (Expr) partialExpr.clone();
return expr.derive(partialName);
}
求导后返回的因子必然为Term类。而在Term类的termSimplify中(即对单项式的化简、展开括号等处理),笔者没有处理term的factor里出现term的情况(求导返回),从而出现bug,返回的term直接被吞掉而导致无输出。
互测发现了房友的一个bug,但是没弄明白原因 ,还被这位房友捅了三刀()
总结
笔者在大二上参加了pre课程,对本学期的oo课程确实有一定帮助,对本学期的java语法有一些基本了解。但是笔者在作业里没有采用pre课程中重点练习的继承(extends),且在设计方面也有很大的不足之处,在第二次作业之后因为害怕写不好放弃了合并同类项导致性能分一路80,也因为没有做好本地测试出现了很多不应该的bug。在下一单元里应该尝试使用评测姬,多多构造数据考虑特殊情况。希望电梯下手轻点(悲