301
社区成员
发帖
与我相关
我的任务
分享





由类图和复杂度分析可知,类内的方法数量含有较多,方法内部的判断条件也较为复杂。虽然做到了架构比较清晰(对于作者而言),但是可扩展性有待提高,仍然有空间去满足更好的高内聚、低耦合。
要求: 对输入表达式进行预处理,并展开括号
1.预处理: 输入的是一串表达式,含,前导0,连续正负号(如“++-”、“-+-”等)。作者首先对输入表达式进行了预处理,即创建initExpr类,专门处理原表达式,利用正则表达式,去、前导0,合并正负号。
2.Lexer: 即词法分析器,分析表达式中每一个字符,并将其抽象为类型,用enum表示,即:
public enum Type {
ADD, SUB, MUL, POW, LP, RP, NUM, VAR
}
创建Lexer类内属性,存储类型及对应具体内容
private ArrayList<Type> types = new ArrayList<>();
private ArrayList<String> contents = new ArrayList<>();
遍历输入表达式,填写ArrayList。
3.Parser: 即语法分析器,在这一部分解析表达式。使用递归下降方法,依次调用parserExpr()、parserTerm()、parserFactor()进行解析。具体调用思路如下:

最终返回一个去括号并化简后的表达式。
4.输出: 经过Parser后得到最终表达式,遍历Term,每个Term设置输出方法。
1.输出的时候注意"0"的特殊性。作者将表达式进行处理后,对其中Term,可能出现Num为0的情况,作者在进行输出时对0进行了不输出处理。但首先,Term的数量不确定;其次,0可能出现在任何一个Term中。若对于任何一个Term都为0的情况,最终将没有任何输出。这个bug在互测时也帮作者刀了别人两下。快哉。
要求: 增加了自定义函数和exp,并支持多层括号嵌套。
1.输入时先解析自定义函数,新创建Definer类,专用于保存传入的函数原表达式,以及在函数调用时,将形参转化为实参
利用两个HashMap,分别保存表达式和形参
private static HashMap<String, String> funcMap = new HashMap<>(); //存储函数定义式
private static HashMap<String, ArrayList<String>> paraMap = new HashMap<>(); //存储函数形参
创建方法callFunc,在函数调用时替换参数,返回字符串。
public static String callFunc(String funcName, ArrayList<Factor> actualParas) {
...
}
2.新建Func类,同样以Factor为接口,当Parser解析到Func时,首先获取函数名和实参,创建Func对象
public Func(String funcName, ArrayList<Factor> actualParas) {
this.newFunc = Definer.callFunc(funcName, actualParas);
this.expr = func2Expr();
}
调用Definer中方法,返回字符串。将该字符串当作第一次作业中输入的表达式字符串,重复Lexer、Parser过程,返回一个Expr。
3.新建Exp类。解析括号中内容时调用parserFactor。

第二次作业是unit1中难度最大的一次。虽然递归过程很明显,但是加入exp后需要实现的计算细节却是很多。一个不小心就会连互测房间也进不去出现很多细节上的失误。
1.深浅拷贝!深浅拷贝!!深浅拷贝!!! (重要的事情说三遍)。作者因此强测寄了很多点,在debug的过程中遭遇了盛大的赛博闹鬼(bushi),这场“闹鬼”作者解决了快一天。原因在于计算exp中表达式因子的过程中,作者写了这样的一段代码:Term term = expr.getTerms.get(i)。显然我们的新term并不是一个“全新的”term。于是就出现了“一行隔过,数字变错”。作者一怒之下怒了一下将所有factor及term中实现了自写的clone()方法进行深拷贝(其实这是一开始就需要完成的工作)。
2.指数范围问题: 由于本次作业支持多层函数嵌套(作者在读题过程中没看到这一点)(真是瞎子),因为我们在第一次作业中,只要是用递归下降思路解决的,括号嵌套自然在第一次的架构中就能解决。因此作者忽略了括号嵌套这一点。但是由于第一次作业限制指数最大不能超过8,作者将指数存为int类型。但一旦支持括号嵌套,会有无数给^8套^8出现,很快就能超过int范围。作者因此寄了强测第二个点(正是考察指数范围的测试点),将int类型变为BigIntegr类型即可。
3.参数替换问题: 替换参数时应该先把形参分别替换为特殊符号如@、!、~等。不能一个个替换。
4.一个不算bug的失误: 作者在第一次作业给自己埋了一个巨大的坑,即在输出过程中是依次回归到expr、term、factor后进行直接输出,没有改写toString方法。这样虽然能够实现输出的目的,但是在函数参数替换的过程中用起来就不如写了toString香(其实压根不能用)。因此作者又为每个类写了toString方法,以实现参数替换。由于作者坚持将摆烂精神发扬到底,并没有弃用原来的print方法,换成新的toString,于是在作者的架构中会发现既有输出方法,也有转换成字符串方法,也导致了类内行数的增加。
要求: 支持函数调用函数,实现求导方法。
1.dx的处理: 将dx(<表达式>)看作一个因子,加入parserDerFactor方法,首先先解析括号内表达式,然后对表达式进行求导,返回求导后的表达式。
2.求导过程: 对表达式求导即对因子依次求导,因此重点在于对因子进行求导。首先经过我们的解析后因子形式为a*x^b*exp(expr),求导后为a*b*x^(b-1)*exp(expr) + a*x^b*exp(expr)*(dx(expr))。对其中的dx(expr)再次递归,直到不存在求导算子为止。计算过程容易实现。
3.函数调用: 按照第二次作业的函数实现方法,本次作业不需要任何操作即可自然完成。并且使用该种方法,只要是定义过的函数都能调用,不只在定义时定义过的函数才能调用。

第三次作业较为简单,一发过而且没被刀,yeah~
奇怪的大数据往往测试不出问题,主要是对于0的处理。
作者仅仅实现了0不输出,1*x...的形式将1省略。
1.感受到了递归下降法的强大之处。学会了使用递归方法解决复杂问题。
2.通过对表达式的分析,强化了自己设计架构的能力。
3.对于通过深浅拷贝的debug引发的思考:出现深浅拷贝的原因在于,我们在计算过程中会出现大量的读、写(详情见wxm大佬的分享及讨论),并应用于不同场景(虽然作者在经历过这次闹鬼后深刻反思了一下,认为在一个完整代码中,是否在计算时只采用“读” 或只采用“写” 会让代码更加优美,并且不容易忘记深浅拷贝的问题,不容易纠结这处到底是需要深拷贝还是不需要(毕竟深拷贝需要的代码量稍多,思路也得拐一下)。毕竟如果我们只“读”,便不需要深拷贝,因为原式永远是原式;但如果我们“写”了,就需要深拷贝,因为原式变了。但仅仅是作者的一点点浅薄的见解(因为作者对于深浅拷贝的问题还是理解的不够通透),作者也将摆烂精神发扬到底,并没有对自己原先的“读写都用”的计算方式进行修改。具体究竟是单一方式更好,还是混合方式更好,交由以后的作者(或者有幸能有伟大的后人看到此篇)有空解决。
第一节课可以在课上讲解一些递归下降的知识。有助于同学们更快上手。