BUAA_OO_第一单元(表达式求导)总结
第一单元作业总共分为3次,属于增量式开发,每次都是在前一次的基础上添加需求。
第一次作业
题目介绍
第一次作业为简单的多项式求导
支持带符号整数,幂函数(底数仅为x)的导函数求解
思路分析
由于之前并没有接触过面向对象的编程,另一方面,由于第一次作业相对来讲比较简单(只需要记录x的指数即可),我从面向过程的思路出发,并没有遇到什么阻碍。换句话说,我并没有由于一Main到底的代码而给自己造成太大的困扰。
重点难点
第一次作业的重难点在于数据结构的选取,由于之前并没有什么java编程知识,在做第一次作业的时候,对treemap的了解并不多,看到第一次实验上机的代码后才明白可以通过指数和项建立键值对的关系来存储。
两种数据结构对比:
用ArrayList数据结构来存储每一项,求导的时候需要遍历每一项。合并同类项时,需要双重循环来比较指数,性能低。
而用treemap数据结构,用指数作key,相同指数(key值)的项自然的就合并在一起了,相较用ArrayList数据结构,由于不需要双重循环来合并同类项,性能大幅提高。
代码分析
一Main到底(参考价值不大)
puliic class Main {
public static void main(String[] args) {
String poly = read();
/*
* 读入字符串,用正则表达式处理
* 包括但不限于:
* 1.去除不可见字符
* 2.将符合规则的多个连续正负号合并为单个正负号
* 3.将"**"(指数符号)替换为"^"(单纯为了便于理解)
* 4. 规范化多项式,如将x变为1*x^1
* */
String [] terms = poly.split("(?<=[x\\d])(?=[+-])");
ArrayList<String> termsArrayList = new ArrayList<>();
// 将规范化的多项式拆分为多个项的和
merge(termArraylist);
// 将同类项合并 只需要比较指数即可, 指数相同即可合并
// 这里我用的Arraylist,需要for循环
// 一种优化就是用treemap,用指数做key,可以大幅提高性能。
solve(termsArrayList);
// 按照求导法则,先将系数乘以指数得到新系数,再将指数减一即可
}
}
bug分析
由于结构和思路都很简单,因此并没有bug
不过我在互测屋中测出了一个同学的BigInteger的bug,输入的指数是BigInteger类型时,该同学不能很好的处理。
第二次作业
题目介绍
第二次作业在第一次作业的基础上增加了简单正余弦(sin(x), cos(x))函数的求导,此外还增加了表达式因子(支持括号嵌套)。
思路分析
由于增加了sin(x),cos(x),因此在处理乘方因子时,不能简单的将指数分离。
另一方面,由于增加了括号嵌套,必然导致递归的产生,因此,我的第一版代码算是踩坑了。只能重构。
通过对多项式的数学形式分析
多项式 = 若干个项的和 项 = 若干个因子的积 因子 = {幂函数因子,三角函数因子,多项式因子}
自然而然的就有以下类:
多项式(Poly类) 项(Term类) 幂函数(Power类) 三角函数(sin类,cos类) 多项式因子,用递归的思想本质还是Poly类
对以上各类的导数形式分析
Poly其导数为Poly类 Term导数为Poly类 ([f(x)g(x)]′=f′(x)g(x)+f(x)g′(x)) Power,sin,cos类导数均为Term类
(f(x)=xn(n 0)时,f′(x)=n * x * (n−1)) sin(x)n 导数为n*cos(x)n-1 cos(x)n导数为-sin(x)n-1
接下来考虑各个类的存储
Poly:若干个Term的和,可以用Arraylist<Term>存储; Term:多个因子的积,具体而言,包括系数,若干个Power因子,若干个Poly因子。 可以分别用BigInteger,Arraylist<Power>,Arraylist<Poly>存储; Power:底数和指数。底数可以为sin类,cos类,X类(这里的X类可以不要,标记即可。当提出了多项式的幂的需求时,在Power;类中包含Poly即可)由于目前而言底数简单(仅为sin(x),cos(x),x,因此可以直接用String类存储底数,但不利于扩展),指数即为BigInteger类型。 sin(cos):目前而言,sin(cos)内仅为x,当有三角函数内部嵌套多项式的需求时,其内可以声明Poly类。
重点难点
第二次作业较第一次作业的一个显著难点就在于,如何处理括号嵌套问题。
括号嵌套问题,由于括号内的子表达式与括号外整个表达式并没有本质区别,因此,自然而然的想到了递归。
由于不会递归下降,我选择的是用栈来存储括号,当栈空时,说明最后匹配到的括号为表达式最外层括号,对于括号内部的表达式,递归分析即可。
代码分析
Main:
public class Main {
public static void main(String[] args) {
String str = read();
Poly poly = new Poly(str);
Poly polyDerivation = poly.derivation();
print(polyDerivation);
}
}
Poly
public class Poly {
private ArrayList<Term> termArrayList;
public Poly(ArrayList<Term> arrayList) {
this.termArrayList = arrayList;
}
public Poly(String str) {
String poly = format(str);
// 格式化字符串,包括去除不可见字符,连续多个+-号的处理等
termArrayList = getTerms(poly);
// 通过poly来解析出每一项,存入Arraylist<Term>中
}
public Poly derivation() {
ArrayList<Term> termArrayList = new ArrayList<>();
for (Term term: this.termArrayList) {
termArrayList.addAll(term.derivation());
}
return new Poly(termArrayList);
}
}
Term
public class Term {
private BigInteger coe;
private ArrayList<Power> powerArrayList;
private ArrayList<Poly> polyArrayList;
public Term(BigInteger coe, ArrayList<Power> powerArrayList, ArrayList<Poly> polyArrayList) {
this.coe = coe;
this.powerArrayList = powerArrayList;
this.polyArrayList = PolyArrayList;
}
public Poly derivation() {
/*
*ArrayList<Power> 和 ArrayList<Poly>中每一个元素单独求导再与其它相乘
*/
return new poly();
}
}
Power
public class Power {
private String base; // 这里我用的String,不好拓展,导致我第三次作业需要做一些改动。
private BigInteger exp;
public Power(String base, BigInteger exp) {
this.base = base;
this.exp = exp;
}
public Term derivation() {
/*
*省略具体实现细节
*/
return new Term();
}
}
以上就构建了代码的初步架构。
至于细节,由于面向对象的思想,因此,只需要关注每个方法内部的实现,而且,也比较好debug。
bug分析
由于我在对输出的表达式的优化阶段考虑不周,正则表达式使用不正确,产生了一个bug,将优化前的表达式结果输出即为正确。
由于我是通过阅读代码测别人的bug,可能别人代码存在bug但我没找到,因此互测屋很和平。
第三次作业
题目介绍
第三次作业在第二次作业的基础上,三角函数允许嵌套任意因子。并且增加了格式检查。即不保证输入数据的合法性。
思路分析
通过第二次作业的处理括号嵌套,三角函数嵌套用同样的思路即可处理。而对于格式检查,我的想法是用正则表达式。
重点难点
这次作业的难点在于格式检查,我使用了正则表达式来检查格式的正确性。
我定义了check方法来检查格式正确性,包括如下检查
-
不允许出现除 +- sin cos () x * 之外的任何字符
-
括号是否匹配
-
符号整数的符号与数不能有不可见字符分隔
-
sin cos等保留字不允许不可见字符
-
**(指数)之间不能有不可见字符
-
数字之间不能有不可见字符
-
sin() cos()内不能出现项(只能是因子)
-
不允许出现数字的乘方,x的x次方,对项的乘方
-
数字与其它因子之间必须有运算符
代码分析
相较于第二次作业,主要变化就是增加了check方法。在后续会讲到check方法,因此此处省略。
bug分析
由于我不是用的递归下降法,因此导致格式检查很复杂。(一个良好的架构能够省去很多不必要的工作量。血泪教训qwq)
这次作业由于格式检查的情况需要考虑很多,我的bug就出现在考虑不全上。
另外,由于我的每一个类对于正确的格式能够处理,换句话说,我的每个类对于WRONG FORMAT敏感,我在每个类中增加了异常处理,这样,即使在格式检查阶段没有检查出来WRONG FORMAT,在后续的处理中会出现Exception。这样,用下述结构即可处理。
try {
//……
} catch (Exception e) {
System.out.println("WRONG FORMAT!");
System.exit(0);
}
度量分析
类图
由于是迭代开发,因此笔者这里只分析第三次作业的程序结构。
Main
方法属性

度量统计

Main中主要方法为check方法,检查合法性。
check方法的作业前面在思路分析中已经提到,不再赘述。
另一方面,从度量统计中可以看到,check方法较为复杂,有待优化(笔者有时间再优化吧)。
Poly
方法属性

度量统计

可以看到这里的getTerms方法复杂度较高,原因是这里用了双重循环来匹配最外层括号,并递归调用。
Term

度量统计

可以看到,代码架构较为合理。
Power

度量统计

Sin

度量统计

Cos

度量统计

逻辑架构如下:

-
缺点:
可以看到,我每个类里面都有derivation方法,应该用一个接口实现更好。
每个类里面方法个数很多,甚至Main类里都存在6个方法,显然代码可读性不高。
由于不熟悉继承类,接口类,因此我的代码还缺少面向对象的思想,不过,我仍旧在努力。
-
优点:
每个类的存在都有必要,每个类只需要完成自己应该做的事情,具备一定的面向对象思想。
思路清晰,自然,易懂。
总结
在OO第一单元作业中,我由于对面向对象的思想不熟悉,因此我的第二次作业是对第一次作业的重构。在重构第二次作业时,我有意识的在思考第三次作业的新需求,因此,我的第三次作业在第二次的基础上基本只需要添加格式检查方法即可拓展。
本次作业有很强的迭代性,深刻地让我了解到了程序可扩展性的重要性。学会预测用户的需求,并预留一定的拓展空间,也是一种能力。
通过阅读互测屋中其它人的代码,我学到了很多,包括如何使用正则,如何使用异常处理,如何自顶向下构建框架。
最后,通过第一单元的训练,我的编程能力,我对面向对象的认识,有了很大的提高,这一过程,痛并快乐着。
浙公网安备 33010602011771号