面向对象第一单元总结:Java实现表达式求导
面向对象第一单元总结:Java实现表达式求导
题目要求
- 输入一个表达式:包含x,x**2,sin(),cos(),等形式,对x求导并输出结果
- 例:\(x+x**2+-2*x**2*(sin(x**2+(cos(x**2))))\)
思路分析
表达式拆解
- 将一个表达式分为三级:表达式
Polynomial、项Term、因子Unit - 一个Poly由Term累加构成,一个Term由Unit累乘构成
- 将一个表达式分为三级:表达式
解题思路
- 读入表达式字符串,将其分解成一个表达式类
- 对表达式类求导
- 输出导数表达式的字符串形式
字符串->表达式
本人采用递归下降的思路,下面简述递归下降算法:
假设一个类A的字符串形式是
"BCD"(拼接),B的形式是"x*E",c是sin(x),D是(A),E是+,那么我们要分析字符串A,就可以这样写public A getA(){
A ans = new A(); B newB = getB();
C newC = getC();
D newD = getD(); ans.addB(newB); ans.addC(newC); ans.addD(newD);
return ans;
}
public B getB(){
B ans = new B(); if(接下来是"x*"){
走2步;
} else {
throw 格式错误异常;
}
E newE = getE(); ans.addE(newE);
return ans;
}
public C getC(){
C ans = null;
if(接下来是"sin(x)"){
ans = new C();
走6步;
} else {
throw 格式错误异常;
}
}
public D getD(){
D ans = null;
if(接下来是"("){
走1步;
} else {
throw 格式错误异常;
}
A newA = getA();
ans.addA(newA);
if(接下来是")"){
走1步
} else {
throw 格式错误异常;
}
return ans;
}
...//剩下的就不写了,主要思路已经阐释清楚
这样,从最大的一级,逐级递归的找更小的构成单元,直到找到以字符为匹配单元的最小单元,由此可以对符合一定语法的字符串进行全面分析
现在再看,我设计的递归下降其实糅合了递归下降和类的生成,这就使得耦合度过高,较为高明的办法应该是使用工厂模式,建立一个
MFactory类,在递归下降中,将匹配到的字符串交由工厂生成一个相应的对象,而不是自己new一个出来
递归下降用到的类
ExpressionString由于在递归下降的过程中,经常需要移动指针、判断字符,因此将这部分功能抽取出来作为一个新的工具类,减小递归下降类的复杂度
成员由一个
String存放表达式,一个Integer存放当前位置ExpressionString实现的方法主要是public void moveSpace(); //跳过当前位置向后的空格
public void moveForward(int step); //位置向前移动step步
public boolean isEnd(); //判断是否读到字符串末尾
public boolean isStartWithChar(char c); //判断是否以c字符开头
public boolean isStartWithString(String s); //判断是否以s字符串开头
...
这样的设计,减少了递归下降过程中代码的重复,增加了可读性,使思路更加明晰
ExpressionParser实现了上面描述的递归下降算法,不再赘述,复杂度如下
Method CogC ev(G) iv(G) v(G) ExpressionParser.ExpressionParser(String)0.0 1.0 1.0 1.0 ExpressionParser.getConstant()0.0 1.0 1.0 1.0 ExpressionParser.getCos()18.0 8.0 10.0 11.0 ExpressionParser.getMultiplicationSign()3.0 3.0 3.0 3.0 ExpressionParser.getOperator()4.0 4.0 4.0 4.0 ExpressionParser.getPoly()1.0 1.0 2.0 2.0 ExpressionParser.getPolyUnit()2.0 2.0 2.0 2.0 ExpressionParser.getPower()12.0 5.0 7.0 8.0 ExpressionParser.getSignedInteger()6.0 3.0 3.0 5.0 ExpressionParser.getSin()18.0 8.0 10.0 11.0 ExpressionParser.getTerm()3.0 1.0 4.0 4.0 ExpressionParser.getUnit()7.0 6.0 8.0 8.0 ExpressionParser.isEnd()0.0 1.0 1.0 1.0 Total 74.0 44.0 56.0 61.0 Average 5.6923076923076925 3.3846153846153846 4.3076923076923075 4.6923076923076925 由于需要分析各种不同类型的格式错误,因此不可避免的在某些方法中的判断语句过多
InputWongFormatException自定义的异常类,继承自
Exception,用于在检测到输入格式错误时抛出异常虽然只需要输出一个
WONG FORMAT!!!,但也要符合面向对象的设计,Java的抛出异常机制非常实用,建议好好学习一下类内虽然只有几行,但也是符合面向对象的代码:
private String exceptionInfor;
public InputWrongFormatException() { //在构造函数中初始化,这样方便应对未来多变的需求
exceptionInfor = "WRONG FORMAT!";
}
public String getExceptionInfor() { //不要将错误信息设置为public,而是留下相应的方法
return exceptionInfor; //这样外部只需要System.out.println(e.getExceptionInfor());即可
}
内核各个类的创建、思路分析
类图:

为了放置和内置库冲突,在自定义的类之前加一个M表示My
MPolynolmial(多项式)MPolynomial类代表了一整个表达式其基本形式是许多项的和,因此也可以看成是一个项的累加类
成员只有一个项的
ArrayListMethod CogC ev(G) iv(G) v(G) MPolynomial.addPoly(MPolynomial)8.0 4.0 5.0 5.0 MPolynomial.addTerm(MTerm)4.0 3.0 4.0 4.0 MPolynomial.addTerm(String,MTerm)2.0 1.0 3.0 3.0 MPolynomial.clone()2.0 1.0 3.0 3.0 MPolynomial.getDerivation()1.0 1.0 2.0 2.0 MPolynomial.MPolynomial()0.0 1.0 1.0 1.0 MPolynomial.toString()6.0 1.0 4.0 6.0 Total 23.0 12.0 22.0 24.0 Average 3.2857142857142856 1.7142857142857142 3.142857142857143 3.4285714285714284 类的主要方法如上,可以看到每一个方法都较为简短
下面以
MPoly中的几个方法说明面向对象的思想getDerivation()由于
MPoly是一个累加类,因此他的导数就等于各个项的导数的和,因此函数体为MPolynomial poly = new MPolynomial();
for (MTerm term : terms) {
MPolynomial derivation = term.getDerivation();
poly.addPoly(derivation);
}
return poly;
MPoly类不会考虑如何得到一个项的导数,他会把这项任务交给一个MTerm(项)去做,MPoly只管跟一个MTerm要他的导数需要注意的是:
MPoly的导数依旧是一个MPoly,因为\((a+b)'=a'+b'\)依旧是一个多项式,只有在Main函数的最后输出时,再调用MPoly的toString()方法获得字符串形式
toString()到得到
MPoly的字符串形式,只需要获得每一项的的字符串,并拼接起来即可其中有一些小小的细节需要考虑,比如项的第一个字符需要是正负号
因此函数体为
String ans = "";
for (MTerm term : terms) { //最重要的6行代码
String next = term.toString();
if (!next.equals("+0") && !next.equals("0")) { //小优化:如果Term是0,则跳过
ans += next;
}
}
if (ans.equals("")) { //如果到最后还是0,则ans为“0”
ans = "0";
}
if (ans.charAt(0) == '+') { //如果第一个字符为'+',则删掉
ans = ans.substring(1);
}
return ans;
addPoly()在
getDerivation()中,我们得到的每一个MTerm的导数都是一个MPoly,每一次需要将新的多项式加到已有的多项式中,因此需要一个addPoly(MPolynomial newPoly)方法我们需要遍历已有的
MTerm,每一次再遍历新的newPoly的所有项,如果两个项能够合并,就合并他们(这其实是一个优化过程,也可以不判断直接加到MTerm的ArrayList中),因此函数体为for (MTerm newTerm : poly.terms) {
boolean foundMergableTerm = false; //记录是否找到了一个可以合并的项
for (MTerm term : terms) {
if (term.isMergable(newTerm)) {
term.merge(newTerm);
foundMergableTerm = true;
break;
}
}
if (!foundMergableTerm) {
terms.add(newTerm);
}
}
相信这几个例子就能够展现面向过程的魅力了。一个类不会过多的考虑除了自己意外的细节,把能交给别人做的事情都交给别人,自己只管提出需求。这样写代码会思路清晰,维护简单且可读性好
MTerm(项)MTerm类代表了一个类其基本形式是许多因子的成绩,因此也可以看成是一个因子的累乘类
成员只有一个因子的
ArrayListMethod CogC ev(G) iv(G) v(G) MTerm.addUnit(MUnit)4.0 3.0 4.0 4.0 MTerm.addUnits(ArrayList)1.0 1.0 2.0 2.0 MTerm.clone()2.0 1.0 3.0 3.0 MTerm.getDerivation()6.0 1.0 4.0 4.0 MTerm.isEmpty()0.0 1.0 1.0 1.0 MTerm.isMergable(MTerm)16.0 4.0 5.0 8.0 MTerm.merge(MTerm)10.0 6.0 2.0 6.0 MTerm.MTerm()0.0 1.0 1.0 1.0 MTerm.toString()9.0 3.0 4.0 8.0 Total 48.0 21.0 26.0 37.0 Average 5.333333333333333 2.3333333333333335 2.888888888888889 4.111111111111111 绝大部分方法依然比较简短,只有在判断两个项是否需要合并时较为复杂,不具有普遍意义
具体的实现就不再赘述,与
MPolynomial的思想是完全相同的,将更基础的事交给底下的MUnit(因子)去做
MUnit(因子)MUnit是所有因子的父类,可以定义为一个抽象类或者是借口,因为我们不需要创建一个MUnit对象,只会创建如cos,sin,x**2等因子对象MUnit规定子类需要实现的方法有:public ArrayList<Munit> getDerivative(); //计算因子自己的导数
public boolean isMergale(MUnit newUnit); //判断自己能否与另一个因子合并
public void merge(MUnit newUnit); //与另一个能够合并的因子合并
public String toString(); //返回自己的字符串形式
public boolean equals(Object o); //判断自己是否与另一个因子相等
public MUnit clone(); //将自己深层克隆一份
MConstant(常数因子)- 幂函数、三角函数、表达式因子等其他因子之前没有“系数”这一成员,而是用一个常数因子作为整个项的系数
- 不在赘述其细节
MPower(幂函数)幂函数主要由指数组成,我的设计中没有“变量”这一个概念,而是用幂函数来代替“变量”,其他的如\(sin(x)**2\)不是幂函数,而是三角函数,三角函数自己有一个指数
因此,如果要增加多变量求偏导数的需求,只需要更改
MPower类的内容,增加成员“变量”,并修改求导、求字符串等函数即可细节不再赘述,其复杂度如下。可以看到也较为简单
Method CogC ev(G) iv(G) v(G) MPower.clone()0.0 1.0 1.0 1.0 MPower.equals(MUnit)1.0 1.0 2.0 2.0 MPower.getDerivative()0.0 1.0 1.0 1.0 MPower.getExponent()0.0 1.0 1.0 1.0 MPower.isMergable(MUnit)0.0 1.0 1.0 1.0 MPower.merge(MUnit)0.0 1.0 1.0 1.0 MPower.MPower(BigInteger)0.0 1.0 1.0 1.0 MPower.MPower(BigInteger,MUnit)0.0 1.0 1.0 1.0 MPower.MPower(String)0.0 1.0 1.0 1.0 MPower.toString()3.0 1.0 3.0 3.0 Total 4.0 10.0 13.0 13.0 Average 0.4 1.0 1.3 1.3
MTrigonometric(三角函数)- 这是
MCos和MSin的父类,只是为了增加程序的扩展性,在这三次作业中没起到任何作用,也没有任何有意义的、与MUnit不重复的方法
- 这是
MSin(sin函数) MCos(cos函数)一个
BigInteger成员作为指数,一个MUnit成员作为内函数(只有第三次作业从单独的x变为一个因子)其余不再赘述,复杂度如下
Method CogC ev(G) iv(G) v(G) MSin.clone()1.0 1.0 2.0 2.0 MSin.equals(MUnit)1.0 1.0 3.0 3.0 MSin.getDerivative()1.0 1.0 2.0 2.0 MSin.isMergable(MUnit)1.0 1.0 2.0 2.0 MSin.merge(MUnit)0.0 1.0 1.0 1.0 MSin.MSin()0.0 1.0 1.0 1.0 MSin.MSin(BigInteger,MUnit)0.0 1.0 1.0 1.0 MSin.toString()3.0 1.0 3.0 3.0 Total 7.0 8.0 15.0 15.0 Average 0.875 1.0 1.875 1.875
MPolyUnit(表达式因子)- 在第二次作业中,加入了表达式因子这种东西,如\(x**2*(sin(x)+3)\)
- 一个
MPolyUnit只有一个MPolynomial成员,两者的本质区别就在于,MPolyUnit继承自MUnit,以及比MPolynomial在toString()中两侧套了一组括号,其余方法均直接调用MPolynomial的同名方法
程序中的bug
面向对象编程的一大特点就是——bug少,而且容易找到。编写内核代码的过程十分顺畅,且几乎不产生bug,即使有,也很快就能找得出。
我认为,原因就在于面向对象编程时,会将一个任务分解成一个个小任务,并且任务之间的耦合度比较低
比如说,我需要完成“表达式求导”这一项任务,那么一个
MPoly需要做的就是以下几步:- 创建一个新的表达式
- 遍历自己的每一项
- 获得每一项的导数
- 将获得的导数添加到新的表达式中
- 遍历完成,返回新的表达式
这五个步骤几乎不涉及到任何的细节问题,而是对任务做出规划,因此这一段代码出bug的概率极低
这样一层一层的将任务细化,最终只有很少部分的方法需要考虑细节问题,大大降低了bug的可能性
测评中出现的bug
- 测评过程中,仅出现过细节方面的错误,比如一个指数比long大、在获得字符串时脑子一抽多写了一句话,导致复杂度为\(n!\)而TLE,均未涉及到内核本质
发现别人程序bug
- 很惭愧,我并没有设计对他人程序的测评机,所以这一部分没有可写的
重构经历总结
- 我并没有看过往届学长学姐的博客,自己从头开始思考的整个结构,很幸运的是三次作业中我基本不需要重构内核代码。
- 第一次作业我使用的是正则表达式分析字符串,第二次作业引入了“表达式因子”这一东西导致正则表达式完全失效,必须要改掉“分析表达式”这一部分的代码。
- 每一次增加新的要求,就把这项要求再拆分成小的需求,在相应的类里添加一小段代码即可实现。这就是面向对象的好处吧。
心得体会
- 从开学第一次OO课开始,需要完成的任务如海一般,一想到这些我就上火,然后就口腔溃疡了一个周没好好吃饭T^T
- 但是OO课的设计确实能让人学到很多很多知识,几次作业迭代的需求也恰到好处的恶心你但是又让你能够做出来,在这个过程中大大提高了OO的意识
- 如果学弟学妹们在OO课之前看到这篇帖子,一定现在立刻马上去学Java,并做完OO的Pre预习任务,否则开学以后苦不堪言。OO一开课,他就默认你精通Java了,不要被Pre里仿佛从0开始教你Java的假象给骗了,学不到精通的程度完全无法应付OO课的编程需求。
- 感谢课程组给了我痛并快乐着的机会:)
面向对象第一单元总结:Java实现表达式求导的更多相关文章
- JAVA实现表达式求导运算的分析总结
1第一次作业 1.1题目描述 对形如4*x+x^2+x的多项式求导. 1.2类图 1.3度量分析 在完成第一次作业时,我的写法没有特别的"面向对象".唯一封装起来的是Node,代表 ...
- BUAA-OO-第一单元表达式求导作业总结
figure:first-child { margin-top: -20px; } #write ol, #write ul { position: relative; } img { max-wid ...
- OO_Unit1_表达式求导总结
OO_Unit1_表达式求导总结 OO的第一单元主要是围绕表达式求导这一问题布置了3个子任务,并在程序的鲁棒性与模型的复杂度上逐渐升级,从而帮助我们更好地提升面向对象的编程能力.事实也证明,通过这 ...
- 2020 OO 第一单元总结 表达式求导
title: BUAA-OO 第一单元总结 date: 2020-03-19 20:53:41 tags: OO categories: 学习 OO第一单元通过三次递进式的作业让我们实现表达式求导,在 ...
- OO面向对象第一单元总结
OO面向对象第一单元总结(表达式求导) 写在前面: 魔鬼课程oo第一单元终于结束,当终究要落笔总结,竟不知从何写起…… 回首再去看第一次的作业,你会满足于那时的幸福,或许,这就是成长吧! 千言万语,一 ...
- 2019年北航OO第1单元(表达式求导)总结
2019年北航OO第1单元(表达式求导)总结 1 基于度量的程序结构分析 量化指标及分析 以下是三次作业的量化指标统计: 关于图中指标在这里简要介绍一下: ev(G):基本复杂度,用来衡量程序非结构化 ...
- Java描述表达式求值的两种解法:双栈结构和二叉树
Java描述表达式求值的两种解法:双栈结构和二叉树 原题大意:表达式求值 求一个非负整数四则混合运算且含嵌套括号表达式的值.如: # 输入: 1+2*(6/2)-4 # 输出: 3.0 数据保证: 保 ...
- OO_JAVA_表达式求导_单元总结
OO_JAVA_表达式求导_单元总结 这里引用个链接,是我写的另一份博客,讲的是设计层面的问题,下面主要是对自己代码的单元总结. 程序分析 (1)基于度量来分析自己的程序结构 第一次作业 程序结构大致 ...
- OO Unit 1 表达式求导
OO Unit 1 表达式求导 面向对象学习小结 前言 本博主要内容目录: 基于度量来分析⾃己的程序结构 缺点反思 重构想法 关于BUG 自己程序出现过的BUG 分析⾃己发现别人程序bug所采⽤的策略 ...
随机推荐
- Java进阶 | IO流核心模块与基本原理
一.IO流与系统 IO技术在JDK中算是极其复杂的模块,其复杂的一个关键原因就是IO操作和系统内核的关联性,另外网络编程,文件管理都依赖IO技术,而且都是编程的难点,想要整体理解IO流,先从Linux ...
- 【Quartz】Quartz存储与持久化-基于quartz.properties的配置
林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 一. Quartz存储与持久化 Quartz提供两种基本作业存储类型.第一种类型叫做RAM ...
- python自定义异常,使用raise引发异常
1.自定义异常类,自定义的异常类必须是Exception或者Error的子类! 1 #!/usr/bin/env python 2 # encoding: utf-8 3 4 class Illega ...
- linux安装subversion
原文: https://www.cnblogs.com/liuxianan/p/linux_install_svn_server.html 安装 使用yum安装非常简单: yum install su ...
- coretext简单使用
相对于UIKit,使用coretext绘制文本效率高,具有更高的自由度,可随时插入图片,增加文本点击事件等. 1.增加文本的点击事件 思路:定义UILabel子类,设置可点击的富文本range及其他属 ...
- ESXi 切换直通导致无法识别硬盘解决
在解决虚机挂载U盘的过程中(已经处理了:VMware中的虚机如何挂载U盘),怎么样都无法加载U盘,故进行了一次操作直通操作的过程中,不小心把所有的存储和控制器全部直通了,导致Esxi主机无法识别到自己 ...
- JUnit5依赖注入与测试接口
依赖注入 以前的JUnit的类构造方法和测试方法都是不能有参数的,JUnit Jupiter有一个颠覆性的改进,就是允许它们有入参,这样就能做依赖注入了. 如果你对pytest的fixture有了解的 ...
- C#/.NET/.NET Core学习视频汇总(持续更新ing)
前言: 之前有很多小伙伴在我的公众号后台留言问有没有C#/.NET/.NET Core这方面相关的视频推荐,我一般都会推荐他们去B站搜索一下.今天刚好有空收集了网上一些比较好的C#/.NET/.NET ...
- Spring RestTemplate 之exchange方法
●exchange方法提供统一的方法模板进行四种请求:POST,PUT,DELETE,GET (1)POST请求 String reqJsonStr = "{\"code\&quo ...
- jmeter测试流程整理
背景 整理jmeter脚本编写流程,注意事项,常用组件,常见问题. 参看链接:https://www.cnblogs.com/pwj2lgx/p/10282422.html 参看:processOn思 ...