第一次作业
写在前面: oopre过去了太久,语法都快忘了个干净,这次作业从零开始狠狠复健了一波~~
说实话,刚拿到这个作业的时候,我基本上没什么思路。虽然前前后后看了好几遍公众号教的递归下降解法,在思考这次作业框架的时候,还是一直纠结于如何处理表达式相乘这样的细节问题,可以说是没能完全理解递归的思想。
接着就去翻了翻学长学姐们的博客,来来回回删改自己的框架,最后历时两天终于确定下来思路,开始码字。于是又码了两天de了一天,终于赶在周五下午写完了。
一、 题目简述
读入一个包含加、减、乘、乘方以及括号(其中括号的深度至多为 1 层)的单变量表达式,输出恒等变形展开所有括号后的表达式。
二、 整体架构
直接上图~:(由于把接口继承之类的知识忘光了所以大类之间的关系十分平平无奇)
框架
这里最基本的思想就是Expr -> Term -> Variable
这样一个树型结构。
下面就大致写一下思路,Lexer
几乎照抄课程组不多赘述了。
1、 Process
- pre-process: 预处理阶段,主要是把输入数据标准化。
process_space();
process_continuousOP();
process_continuousOP(); //调用两遍是因为可能出现三个连续符号
process_format(); //处理一下"^+"和在括号前加"1*"
- post-process: 输出前处理阶段,主要是简化输出。在写完之后看到群友讨论才意识到第一项放正项会比负项少一个符号,第二次作业优化了一下这个问题。
linkExpr();
process_continuousOP();
result.replaceAll("\\+0", "");
result.replaceAll("-0", "");
delete_firstOne();
put_zero_ifEmpty();
2、 Variable
从UML图上可以看到,变量类有四个属性
- type: 变量的类型,这里我把变量因子和常量因子都塞在这个大类里(其实应该用继承的,但是太久没写已经忘掉了,后面迭代的时候非常痛苦)
- base: 底数,是一个字符串,用来应对后面可能出现的多变量,常数的话这里为该数的字符串。(第三次作业由于还是没出现多变量,于是把这个属性删了)
- symbol: 符号,由于这个类是不带系数的,所以引入这个属性来读入前面的
+/-
- pow: 指数,常数这里是0;变量的话读取
^
的下一个token
3、 Term
这是最大也是最复杂的一个类。属性类似于上一个,就不展开了,多了一个coefficient表示系数。众所周知,项是由很多个因子连乘而来(先不考虑表达式因子),所以我给它定义的一般结构是coefficient * base ^ pow
。
当然,除了这三个参数外,还有一个symbol
,同上一条,是用来存这个一整个项的正负。
4、 Expr
比起Term
,我给Expr
的任务就很简单了,其中就是一个ArrayList<Term>
的集合,说白了就是很多个形如3 * x ^ 2
、123 * x ^ 0
这样的项加起来,由于项本身自带符号位(也就是把+/-
读进项的解析中),Expr
里不用考虑加减问题,无脑加就行了~
5、 Parser
可以说整个代码最核心的工作就在这里了。也就是这里的递归把自己给绕进去了,理解了一整天才绕出来。
先贴一下大致的框架 ↓
public ArrayList<Term> parseExpr() {
while ('+' | '-') {
expr.addTerm(parseTerm());
}
simplify_Expr(Expr.getTerms()); //化简:先合并同类项再排序
} //最后我们可以得到一个只有最简项的ArrayList~
private ArrayList<Term> parseTerm(int symbol) {
new mids;
results = 第一个因子;
while ('*') {
if ('(') mids.add(parseExpr());
else mids.add(parseVariable());
result = multiply(results,mids);
}
}
private Variable parseVariable() {
setType();
setSymbol();
setBase();
setPow();
}
这里有一个点: 我的Expr和Term解析方法返回的都是一个Term的ArrayList。Expr
很好理解就不多说了,那么为什么parseTerm
也返回一个容器呢?因为因子中有一项叫表达式因子
,这个表达式因子和前面的因子相乘,会出现形如2 * x ^ 3 + 5 * x ^ 1 - 6
这样的一串表达式。而parseTerm
这个方法存在的意义就是处理一连串东西相乘。所以这个时候,简简单单一个Term类型的返回值显然是不够的。使用ArrayList<Term>
不仅可以应对有括号的情况,在单项的情况下也可以通过让容器里只有一个元素来实现,更有普遍意义。再者,通过这样一种方法,所有的需要相乘的乘数都被转化成了ArrayList<Term>
类型,所以写multiply
方法的时候就很方便了,不用再去区分什么项*项
,项*表达式
,表达式*表达式
了。(嗯,但是这个有点面向过程了)
以下是乘法的一个简单实现,放在Calculator类里面,遍历两遍就ok了:
public ArrayList<Term> multiply(ArrayList<Term> terms1, ArrayList<Term> terms2) {
for (Term value1 : terms1) {
for (Term value2 : terms2) {
multiply_term(value1, value2);
}
}
}
综上所述,我这样的架构其实就是实现起来比较方便,缺点是可扩展性不好。
6、 Simpifier
主要是针对表达式的化简,每解析出一个表达式(包括表达式因子)就化简一次
public ArrayList<Term> simplify_Expr(ArrayList<Term> terms) {
merge(); //合并同类项
sort(); //排序,虽然一个字不会但自动化全给我生成出来了
}
现在想想好像Calculator
和Simplifier
里面我没放属性只放了方法,而且这两个类都很简洁,其实可以把这种类整合成一个Tools
三、 bug分析
这次的中测过得难以置信地顺利,不过互测阶段因为一个小bug被刀了好多次。
由于新手上路还不会搭评测机,所以自己测试和刀人的数据全靠手搓。不过,自己测试的时候并没有在意互测要求的50字符长度限制,互测的时候不符合形式化表述了三次才意识到这个问题(认真审题!!)。然后在某一次尝试整点阴间数据的时候,拿自己的代码跑了一遍,发现输出了一些不应该出现的东西:比如-4*x^7+1x^6
,于是吓得连忙检查了一下代码,发现是postprocess中,无脑把1*
替换成了空字符。这个bug导致了只要正确答案里有11*
、21*
这种系数就会产生错误。也难怪被7/64了(哭)。
以及还有一些自己测试的时候暴露出来的问题,粗心把系数指数搞混什么的就不说了,最有代表性的还是到底什么时候要调用lexer.next(),这个问题也是由于一层层递归而导致自己的cpu过载,理不清其中的关系。当然一种方法是面向结果编程,暴力尝试。不优雅,但是很有效。 嗯,正确的做法当然是想清楚pos相对于token的位置,最好整个程序是统一的。我这里采用的是在进入下一个parser之前pos放到下一个token的地方,出来的时候pos再往后挪一个,也就是pos一直放在没有处理过的位置。
四、 反思与总结
写这次作业时我没有特意去在意括号嵌套的问题,不过后来随便搓几个数据试了一下发现似乎是支持的。于是又重新阅读理解了一下代码,发现由于一层层的嵌套结构存在,当读到双层甚至多层括号时,依次调用方法parseExpr -> parseTerm -> parseBracket -> parseExpr
,而这里的parseExpr
返回的永远是包含表达式中所有最简项的一个容器。所以在parseBracket
读到括号和读到一般的项时,所得到的返回值都是一样的,也就不需要额外的动作来实现嵌套了。
说说一些不足之处吧。第一点是我并没有严格按照课程组给出的结构,将因子分为三个部分,实现统一的接口。这就导致了我的代码在判断类型时分支有些多,Parser
类也显得比较冗长。还有一点就是无脑get和set了很多属性,导致了有些private属性不那么"private",这也是老师上课强调过的一个问题。
第二次作业
写在前面: 第二次作业相比于第一次作业,虽然难度有所提升,但由于不用像第一次作业一样从零开始搭建整体架构,轻松了不少。还有就是在研讨课上听了大佬分享自己的思路,受益匪浅。
一、题目简述
比起上次的作业,这次在因子上做了些文章,加入了自定义函数因子和指数函数因子
-
指数函数
- 一般形式: 类似于幂函数,由
exp(<因子>)
、指数符号^
和指数组成,其中:指数为符号不是-
的整数,如:exp(x) ^ +2
。 - 省略形式: 当
exp()
这一整体的指数为 1 的时候,即exp(...)^1
,可以采用省略形式,省略指数符号^
和指数部分,如:exp(x)
。
- 一般形式: 类似于幂函数,由
-
自定义函数
- 自定义函数的定义形如
f(x, y, z) = 函数表达式
,比如f(y) = y\^2
,g(x, y) = exp(x)*exp(y\^2)
,h(x, y, z) = x + y + z
。 - f 、g、h 是函数的函数名。在本次作业中,保证函数名只使用 f ,g,h,且不出现同名函数的重复定义。
- x、y、z 为函数的形参。在本次作业中,形参个数为 1~3 个。形参只使用 - x,y,z,且同一函数定义中不会出现重复使用的形参。
- 函数表达式为一个关于形参的表达式。函数表达式的一般形式参见形式化定义。
- 自定义函数的调用形如
f(因子, 因子, 因子)
,比如f(x\^2)
,g(exp(x\^2), exp(x))
,h(1, 0, -1)
。 - 因子为函数调用时的实参,包含任意一种因子。
- 自定义函数的定义形如
二、整体架构
(不是很会调uml图的位置)
总体来说和第一次差的不多,我也没有进行什么规模很大的重构,就是把一些写得很混乱的方法重新梳理了一下~~(代码风格不好迭代起来真的很痛苦T-T)
改动
因为我第一次作业的Simplifier
类和Calculator
类都只提供了方法,没有自身的属性,所以把它们合并归入Tools
类,就不用实例化两个对象了。然后因为这次性能分比较难卷,我只把上次作业遗留问题,正项提前解决了一下。
增加
这次作业主要有三个任务:括号嵌套、指数函数、自定义函数
其中括号嵌套,只要第一次作业用的是递归下降,就不会有什么大问题,所以着重说说另外两个。
我个人是先实现的自定义函数,因为它可以完全放在预处理部分解决,不会需要更改Parser类内容,涉及范围比较小,也不太容易引出一些奇奇怪怪的联动bug。
先丢一个初步思考↓ (除了自己没人看得懂系列)
大致的逻辑是:先读入函数的形参表达式,用HashList
存下来,key
值是函数名。这里需要把所有的x替换成u(小心exp),避免后续重复替换。然后,扫描待解析表达式,如果扫到函数名,存下这个位置。然后向后扫描,检测到,
或者)
,将这中间的因子读出,替换掉函数解析式的形参,并将这个替换后的函数解析式替换回原表达式。
(后期吐槽:c语言根深蒂固了已经)
public String rpl_func(input, funcDef) {
while ("f" | "g" | "h") {
for (readFunc) {
if (!("f" | "g" | "h")) { continue; }
for (readFunc) {
if ('(') { bracket_pull; }
else if (')' && bracketMatch) { replace(); break; }
else if (')') { bracket_push; }
else if (',' && bracketMatch) { replace(); }
}
}
}
}
然后是对exp的解析。我的方法是:在Term类里面加入ExpBase
和ExpPow
两个属性,其中ExpBase
用arrayList<Term>
表示(开始套娃)。因为exp括号里的东西一定是因子,所以就可以调用parseTerm()
来解析。exp的指数部分,需要乘进base
中的每一项,然后设置成1,便于化简。所以,这里就需要更改parseTerm()
、multiply()
、和simplify()
方法,来实现包含指数函数的多项式运算与合并。这里面需要新增expEqual()
方法来判断exp内容是否相等,以及改写一下Term类的equals()
方法,由于Term类中含ArrayList<Term>
,所以要递归判断。
@Override
public boolean equals(Object obj) {
return property == obj.property && aryListsEqual(expBase, obj.expBase);
}
private boolean aryListsEqual(ArrayList<Term> list1, ArrayList<Term> list2) {
for (i < length) {
if (!list1.get(i).equals(list2.get(i))) {
return false;
}
}
return true;
}
三、bug分析
因为有些类比如Term
我没有设置toString
方法,无论是调试还是print大法都很难查看。所以只好print("hello")
一步步排查。
当lexer.peek() == 'exp'
时,我对后面的内容调用了一次parseBracket()
,就是专门解析表达式因子的方法,但是这种方法将括号后紧跟的指数识别成了表达式因子的幂次,从而进行了乘方运算,与exp所需要的行为不同。也就是说,exp(6)^2
应该是exp(12)
,但是我会输出exp(36)
。
还有,写*.get()
的时候一定要先判断这个内容是不是为空,否则很容易出现NullPointerException
。特别是当很多项合并成0的时候,清空了List,这时候再去调用有关List的方法比如判断相等的方法,就会报re
。
这次交的强测版本有俩bug,并且强测都没测出来,被分到A房,所以互测寄得很惨,给同学们送了一波分。第一,我原本的合并同类项这个操作,当两个项相加等于零时,就删除这两项。最后在输出的时候判断若表达式内部没有项时,输出0。但是,随着exp的加入,我没有对exp内的表达式进行空串判断,导致了exp((1-1))
这种输入无法处理,会输出一个exp(())
。第二,我在函数调用的括号判断时,忘了分析第一个实参的左括号,导致了读入参数时混乱。比如f(1+2,3)
可以读入,f((1+2),3)
就会出错。
四、总结与反思
这次作业的难度较高,但是有了第一次的惨痛教训,还是留出了一些时间提高代码可读性,为下一次迭代铺路。可惜的是,由于架构原因,很难判断exp()内的内容是否为表达式,可不可以去掉一层括号,也因此损失了一些性能分。
这里其实本来的想法是判断括号内ArrayList所含项数,但后来仔细一想发现exp(2*x)也是不合法的,再增加更多判断条件的话显得代码很臃肿,于是开摆
这个问题也是架构不合理所导致的,因为没有按照因子的定义来实现接口,最终也无法用很好的方式来实现对exp括号内容类型的判断。
第三次作业
这次作业比起前面的来说,算是比较简单的了~
一、题目要求
这次作业主要是新增了两个任务:函数定义中的函数调用、求导因子
求导因子:
- 求导因子 → 求导算子 空白项 ‘(’ 空白项 求导因子 空白项 ‘)’ | 求导算子 空白项 ‘(’ 空白项 表达式 空白项 ‘)’
- 求导算子 → ‘dx’
二、整体架构
基本结构跟上次差不多。
首先,函数定义调用跟表达式中调用操作一样,放在预处理中字符串替换就可以了,读取完成函数表达式后,调用一个替换方法,不需要其它操作。
表达式求导,按照的结构,只需要实现系数*x^指数*exp(……)^指数
这个算式的求导。把它分成两个部分系数*x^指数
,exp(……)^指数
分别求导,再利用乘法公式加起来。
public ArrayList<Term> derive_Expr(ArrayList<Term> input) {
for (Term term: input) {
results.addAll(derive_Term(term));
}
}
public ArrayList<Term> derive_Term(Term input) {
Var = input_var();
Exp = input_exp();
Var_d = derive_Var(input);
Exp_d = derive_Exp(input);
results.addAll(multiply(Var, Exp_d));
results.addAll(multiply(Var_d, Exp));
}
三、bug分析
中测的时候没有发现什么特别大的问题。不过后来在优化的过程中,不小心把addAll方法当成深克隆用了,导致了乘法的时候用结果替换了第一个乘数,再次利用该乘数时,产生错误输出。
这次互测因为有两个bug又被集火。第一个问题就是读函数的时候忘记处理空格了(自己搓测试点的时候习惯性地忽略了这一点hhh)。第二点是,这一次函数嵌套调用时,不仅仅是x会被覆盖,y和z也会被覆盖产生错误,所以有必要将y和z也替换成别的字母。
四、总结与反思
本人写代码的习惯不是很好,一般是想到哪里写到哪里,所以这三次的作业我基本上还是面向过程的思想居多。周三上机看了课程组提供的代码后,我逐渐理解了面向对象的一大特点:可读性强。所以,之后的作业,我会从第一次迭代开始,就搭建起良好的架构,锻炼自己的oo能力。
复杂度分析
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
expr.Expr.addTerm(ArrayList) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Expr.Expr() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Expr.getTerms() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Expr.merge_terms() | 8.0 | 5.0 | 6.0 | 6.0 |
expr.Expr.toString() | 3.0 | 1.0 | 3.0 | 3.0 |
expr.Func.addParameters(String) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Func.Func() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Func.getFuncName() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Func.getFunction() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Func.getParas() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Func.setFuncName(String) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Func.setFunction(String) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.add_coefficient(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.aryListsEqual(ArrayList, ArrayList) | 7.0 | 5.0 | 4.0 | 8.0 |
expr.Term.equals(Object) | 4.0 | 3.0 | 7.0 | 9.0 |
expr.Term.getBase() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.getCoe() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.getEB() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.getEP() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.getPow() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.getSymbol() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.hashCode() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.setBase(String) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.setCoefficient(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.setPow(BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.setSymbol(int) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.Term(int, BigInteger, BigInteger, ArrayList, BigInteger) | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Term.toString() | 3.0 | 1.0 | 3.0 | 3.0 |
expr.Variable.getBase() | 1.0 | 2.0 | 2.0 | 2.0 |
expr.Variable.getPow() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Variable.getSymbol() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Variable.getType() | 0.0 | 1.0 | 1.0 | 1.0 |
expr.Variable.toString() | 8.0 | 4.0 | 1.0 | 4.0 |
expr.Variable.Variable(int, int, BigInteger, String) | 2.0 | 1.0 | 2.0 | 2.0 |
Input.Input() | 0.0 | 1.0 | 1.0 | 1.0 |
Input.readExpr(Process) | 0.0 | 1.0 | 1.0 | 1.0 |
Input.readFunc() | 15.0 | 6.0 | 9.0 | 9.0 |
Input.readNum() | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.get_number() | 2.0 | 1.0 | 4.0 | 4.0 |
Lexer.Lexer(String) | 0.0 | 1.0 | 1.0 | 1.0 |
Lexer.next() | 6.0 | 2.0 | 5.0 | 6.0 |
Lexer.peek() | 0.0 | 1.0 | 1.0 | 1.0 |
MainClass.main(String[]) | 0.0 | 1.0 | 1.0 | 1.0 |
Parser.parseBracket(ArrayList) | 1.0 | 1.0 | 2.0 | 2.0 |
Parser.parseDx() | 0.0 | 1.0 | 1.0 | 1.0 |
Parser.parseExp() | 3.0 | 2.0 | 3.0 | 4.0 |
Parser.parseExpr() | 10.0 | 1.0 | 6.0 | 8.0 |
Parser.parsePow() | 2.0 | 2.0 | 2.0 | 2.0 |
Parser.Parser(Lexer, HashMap<String, Func>) | 0.0 | 1.0 | 1.0 | 1.0 |
Parser.parseTerm(int) | 16.0 | 1.0 | 8.0 | 8.0 |
Parser.parseVariable() | 6.0 | 3.0 | 4.0 | 5.0 |
Process.postprocess(ArrayList) | 2.0 | 2.0 | 1.0 | 2.0 |
Process.preprocess(String) | 0.0 | 1.0 | 1.0 | 1.0 |
Process.preprocessFunc(String) | 0.0 | 1.0 | 1.0 | 1.0 |
Process.Process(HashMap<String, Func>) | 0.0 | 1.0 | 1.0 | 1.0 |
Process.process_continuousOP(String) | 0.0 | 1.0 | 1.0 | 1.0 |
Process.process_function(String, HashMap<String, Func>, int) | 22.0 | 7.0 | 15.0 | 16.0 |
Process.process_space(String) | 0.0 | 1.0 | 1.0 | 1.0 |
Process.rplSubstring(String, int, int, String) | 0.0 | 1.0 | 1.0 | 1.0 |
Tools.addExp(Term, Term) | 6.0 | 3.0 | 5.0 | 5.0 |
Tools.derive_Exp(Term) | 1.0 | 2.0 | 2.0 | 2.0 |
Tools.derive_Expr(ArrayList) | 1.0 | 1.0 | 2.0 | 2.0 |
Tools.derive_Term(Term) | 1.0 | 1.0 | 2.0 | 2.0 |
Tools.expEqual(ArrayList, ArrayList) | 11.0 | 6.0 | 5.0 | 8.0 |
Tools.linkExpr(ArrayList) | 31.0 | 3.0 | 11.0 | 12.0 |
Tools.merge(ArrayList) | 23.0 | 4.0 | 12.0 | 12.0 |
Tools.multiply(ArrayList, ArrayList) | 18.0 | 1.0 | 5.0 | 7.0 |
Tools.my_addAll(ArrayList, ArrayList) | 1.0 | 1.0 | 2.0 | 2.0 |
Tools.Simplify_exp(Term) | 4.0 | 3.0 | 4.0 | 4.0 |
Tools.simplify_Expr(ArrayList) | 1.0 | 1.0 | 2.0 | 2.0 |
Tools.sort(ArrayList) | 0.0 | 1.0 | 1.0 | 1.0 |
Tools.sortSymbol(ArrayList) | 0.0 | 1.0 | 1.0 | 1.0 |
Total | 219.0 | 118.0 | 181.0 | 203.0 |
Average | 3.0416 | 1.6389 | 2.5139 | 2.8194 |
我这些复杂度很高的方法,比如linkExpr
,parseTerm
,process_function
,multiply
,基本上都是以面向过程的方法写的,所以增加了程序调试和维护的负担,这也是一个我在之后的学习中需要改进的点。
各个类的行数控制地还可以,Tools
类比较多是因为我把一些化简、计算的方法都放进来了,下次可以按照功能再细分一下。
未来方向
这一单元的作业第一次带来的压力是比较大的。毕竟过了半个学期+一个寒假,上来就从零开始写出一整个程序,对于我这种基础不扎实的同学来说是一个不小的挑战。所以我希望第一次作业可以适当地循序渐进一下,在课上讲解一下大致框架之类的可能会对我们写作业有很大帮助~~