oo第一单元的作业主要围绕表达式的匹配、处理和输出进行,三次作业对因子的定义逐渐细化,从开始的只包含幂次方和常数项、第二次的包含三角函数、到第三次包含嵌套结构。下面对三次作业的情况分别进行总结。
一、第一次作业
1、作业内容和思路
第一次作业考察的是最简单的多项式求导,只包含常数项和幂次方,性能上要求尽量取得最短长度的正确结果。第一次接触面向对象,对类的理解不是很成熟,代码比较接近于面向过程的风格,只设计了一个Polynomial类和一个Termnode类,分别代表一个多项式整体和多项式中的一项,其中,Polynomial的成员为一个Termnode的可变长列表,Termnode的成员包含常数项和幂次方两个属性,由于题目中要求处理大整数,因此都采用BigInteger类型处理。
字符串的处理上,开始尝试用正则表达式匹配整个字符串,如下
String signint = "[+-]?\\d+"; String powerfunction = "x[ \\t]*([\\^][ \\t]*" + signint + ")?"; String varterm = "(" + signint + "[ \\t]*\\*|[+-]?)[ \\t]*" +powerfunction; String term0 = "^[ \\t]*[+-]?[ \\t]*(" + varterm + "|" + signint +")[ \\t]*"; String term = "[ \\t]*[+-][ \\t]*(" + varterm + "|" + signint +")[ \\t]*"; String poly = term0+"("+term+")*";
如此匹配后发现若输入很长(比如若干个x相加)会发生爆栈的情况,查阅解决办法得知是由于正则表达式过长,因此只用正则表达式匹配term级别的字符串,其余部分用下推自动机的思路编写。
优化上,向表达式中插入一个term时会检查有无能够合并的项,输出时充分考虑可以省略的0和1,并按照系数的降序排列(为了将-1+x优化为x-1缩短一个单位)。
2、UML设计
uml图如下
3、度量分析
从分析中可以看到大多数复杂度不是很大,在Poly的构造方法处稍大一些,这是由于使用了下推自动机的关系,在toString方法上,循环和条件判定比较多,将优化和输出同时放在这里所以复杂度较大。
4、总结
总的来说第一次作业的代码量不是很大,花费时间也不是很长,大概用时一个晚上,然而由于是面向过程的思路,为后面的重构埋下了伏笔。
二、第二次作业
1、作业内容和思路
第二次作业在第一次的基础上,因子加入了sin和cos函数,好在作用对象只能为x,在不考虑扩展性的情况下,可以将x,sin(x),cos(x)看成整体,每个项变得更为复杂,总的来说包含系数和指数元组两部分,系数为一个常数项,而指数三元组分别代表x,sin(x)和cos(x)的指数,依然用BigInteger来写。老师上课时提示尽量使用面向对象的思路和方法来写代码,因此我尝试将poly类,term类和factor类共有的方法写在类外面并用visitor模式来让这些类使用这些方法。简单来说是这些类都实现一个Node接口包含accept方法,而所有的操作都要实现Operator<T>的接口,接口中定义了对所有node的抽象操作方法。代码实现示例如下(只包含了部分类方法)。
public interface Node { <T> T accept(Operator<T> operator); } public interface Operator<T> { T op(Var x); T op(CosF x); T op(Factor x);
... } public class Poly implements Node { @Override public <T> T accept(Operator<T> operator) { return operator.op(this); } ... } public class Tostring implements Operator<String> { @Override public String op(Factor x) { if (x instanceof PowF) { return op((PowF) x); } else if (x instanceof SinF) { return op((SinF) x); } else if (x instanceof CosF) { return op((CosF) x); } else if (x instanceof Const) { return op((Const) x); } else if (x instanceof Var) { return op((Var) x); } return "WRONG!!"; } @Override public String op(CosF x) { return "cos(x)"; } @Override public String op(Var x) { return "x"; }
... }
如此设计后,实现Node接口的类有多项式类、项类、因子类,而实现了Operator接口的类有用于识别字符串的,求导的,输出的等。
2、UML设计
uml图如下
3、度量分析
从分析中可以看到,由于将一些通用方法放在了类外,操作的循环复杂度相比于第一次作业有了一定的减小。复杂度最大的方法依然是Pharser和ToString方法,其中涉及了比较复杂的条件判断来。
4、总结
这次作业我为了实现visitor模式花费了很久的精力,本来计划将factor细分以实现函数的嵌套功能(下一次作业的要求)但由于时间关系,没有写完,最后一版的代码没有使用factor类,在term类中完成了所有的操作(用常数项和指数项三元组),这个操作在下一次作业中完全没有扩展性,因此下一次作业主要在重构这部分代码。
由于一直在纠结函数嵌套关系,我没能完成对三角函数的优化,优化仅仅停留在合并同类项上。因此性能分扣了不少。查看了强测样例我发现,很多肉眼难以看出的三角函数优化,作业后我思考如何实现这种优化,一种可能的方式是尝试用欧拉公式变形后合并同类项。本想在第三次作业中实践一下这种优化,发现第三次作业所需的优化更加复杂索性最终放弃了。
三、第三次作业
1、作业内容和思路
第三次作业最复杂的地方在于如何处理函数的嵌套关系,特化在功能上主要是如何识别带括号的字符串、处理嵌套函数求导、合并同类项。
对于如何处理字符串,由于一直是使用下推自动机为主体的结构因此不需要改动太多,只需要在factor级别加入一种括号多项式的分支即可。
对于嵌套函数的求导,由于被嵌套的因子不一定是简单因子还可能是括号多项式的复杂因子,但在求导上都可以泛化为锁链法则,因此在这一步,无论因子是否是简单因子都对其求导,返回一个项,至于化简问题在合并同类项中解决。
因子之间的合并变得比第二次作业复杂很多,因为第二次作业只有可能出现x,sin(x),cos(x)及他们的幂次方的形式,所以判断起来很容易,这次作业需要判断两个因子是否可以相乘并可以计算其惩罚结果,因此采用的方法是让不同的因子的类继承父类Factor,包含multable和mult方法,代码示例如下。
public abstract class Factor implements Node { @Override public <T> T accept(Operator<T> operator) { return operator.op(this); } public abstract Factor mult(Factor b); public abstract boolean multable(Factor x); public abstract Factor copy(); }
2、UML设计
uml图如下
类主要有三块,第一块都实现Node接口以便接受Operator,包括多项式、项、因子、各种简单因子和嵌套因子。第二块是Operator接口,包括求导和输出,第三块为Pharser用于接收字符串生成多项式。后来考虑可以将Pharser作为静态类封装在Poly中,构造函数中使用Pharser类可能会更好。
3、度量分析
将factor细化后,方法数目变多了很多,但是每个方法细化下去也没有很复杂,只负责本类的实现方法。因此平均复杂度反而有所下降。
4、总结
这次作业花的时间主要在于设计上,把上一次作业没有使用的factor类编写完了。编程过程并没有很久,设计好了之后很快就写完了,并且调试也没有太费劲,只有几处比较好查的笔误。的确一个好的设计可以减少工作量。
优化上,除了合并同类项和处理0和1的问题,主要解决了当括号多项式中只包含一个因子时候的化简,可以减少不必要的括号,没有实现的是括号中只有一个项的化简,因此强测中扣分的点都是这种情况。
四、总结
这次作业结束了,我开始尝试用类和接口的概念去设计一个程序,这算是一个进步吧。但是有时候无法将实际问题快速的抽象成类来实现,编写代码时,有时还不清楚运行时会调用哪个重写的函数,每次遇到拿不准的地方都要重新做实验,这方面还需要多练习多体会。在之后的作业中我会继续努力练习。若有遗漏或错误的叙述,还望多多指教!