第一个单元的三次作业均为求导,循序渐进的让我们掌握如何构造类和方法,让整个代码是面向对象的设计而不是面向过程的设计。如果第一次作业和第二次作业你只是简单的对过程着手架构类,到了第三次作业就会变得格外麻烦。掌握了面向对象创建多个类、分层次地实现每个类的功能,并梳理清楚继承与接口处理每个类的思路,便能够游刃有余地解决。
一. 总体设计思路
第一次作业
第一次作业时,还没有建立面向对象的程序设计思维和架构类的思路,因此整个代码采用面向过程,只构建了一个类,用多个方法划分,来处理输入并打印出求导的结果。
(1)构建方法wrong来处理输入,判断是否输出WF,如果不是则继续求导,而在wrong方法内用正则表达式按[+-]匹配拆分的方式匹配(不采用一个大正则来匹配WF来防止爆栈)
(2)将式中所有的空格和\t用空串取代,然后用正则表达式match类和group类提取出*分开的每一项,并调用方法xishu和zhishu将每一项的系数和指数存入Arraylist中,在求导前先调用Hebing方法进行合并同类项进行化简。
(3)将合并后的每一项的系数和指数通过方法derivate进行求导,输出每一项求导后的字符串,在方法中判断如果系数为0,则为空字符串,如果系数为1或-1,则约去“1*”,如果指数为0,则输出系数,如果指数为1则约去指数“^指数”。进行化简操作。
(4)打印:
1. 将系数大于0的项(如果有的话)换到最前面(表达式长度-1)
3. 如果不是第一项且此项为正,输出一个正号。
5. 如果最终结果为空字符串,则输出0。
第二次作业
(1)在Polymonomial类中,与第一次作业同样的用[+-]划分逐项匹配,判断合法性来输出WF。
(2)将表达式逐项拆解,用monomial类提取表达式中每一项的系数,x的指数,sin(x)的指数和cos(x)的指数。
(3)将monomial类提取出来的四个参数传入derivation类,先分别对x,sin(x),cos(x)求导,然后根据乘的求导法则,组合在一起,再前面加上“xishu*”,如果系数为0,则为空字符串,如果为1或-1,则省略掉“1*”。
(4)将求导后的字符串组合在一起,再用hebing类进行合并同类项。
(5)打印规则如第一次作业。
第三次作业
(1)按照表达式(expression)、单项式(monomial)、因子(factor)的层次,分别建立三个类,进行递归的求导和合法性判断。
(2)将输入通过expression类进行[+-]的划分,划分的每一项传入monomial类,在monomial类里进行*的划分,每一项传入factor类,在factor类进行因子判断,如果为普通因子如x,sin(x)和cos(x),则根据求导法则进行求导,将结果字符串return回去;如果存在嵌套括号,则判断其为嵌套因子,通过正则匹配group提取出嵌套的因子,递归用factor处理,并return其求导结果;如果是表达式因子,则传入expression类,进行递归return其求导结果。
(3)逐层递归地返回结果为字符串类型,最终在main里完成输出。
(4)打印规则如第一次作业。
二. 自我测试发现的bug
第一次作业
(1)爆栈错误
开始时判断WF,采用的是通过一整个大字符串来进行匹配是否配对,当输入500个“+x”后出现了爆栈的错误,故修改wrong方法,通过[+-]进行划分逐项匹配,用一个小字符串正则匹配每一项,最终成功解决了爆栈问题。
(2)划分错误
划分采用的是通过[+-]分开,但[+-]不仅会出现在项与项之间,还会出现在指数为有符号整数里,故这里采用replaceAll将所有的“^+”和“^-”替换为“^zheng”和“^fu”,分开以后,再将所有的“zheng”和“fu”替换为相应的正负号,以免出现错误。
(3)优化改进
在优化过程中,通过与同学讨论,除合并同类项外,还可以在最终结果中寻找系数为正数的项,如果寻找到,则将其提前到首项再省去第一个字符“+”。故遍历最终结果的每一项, 将找到的第一个正项与首项互换即可。
第二次作业
(1)无输入情况错误
中测通过一个错误数据点发现无输入报错的问题,修改input方法,用hasNextLine()判断是否有输入,如有则进行操作,无则输出WF,解决了这个问题。
(2)正负号个数问题的处理
过了中测以后,通过自己设计测试数据,发现了对于正负号的判断,后面接整数和后面接x/sin(x)/cos(x)情况有区别,在PolyDerivationpolynomomial类的wrong方法里增加特殊情况判断,解决此情况。
(3)划分错误
相比于第一次作业,这里会出现[+-]的情况还有每一项连乘之间的有符号整数,因此增加replaceAll另外将所有的“*+”和“*-”替换为“*zheng”和“*fu”,分开以后,再将所有的“zheng”和“fu”替换为相应的正负号,以免出现错误。
第三次作业
在构建类和递归方法过程中没有很大问题,很快便能够正常的处理嵌套因子和表达式因子的各种求导结果,但仍旧出现了一些问题。
(1)输入中出现不必要的括号
在写好求导类的处理并准备判断合法性时,发现当输入“((sin(x)*x))”时,进入monomial类处理时,用乘号*进行划分每一项分别是“((sin(x)”和“x))”,无法正常进入factor类求导,最终会输出WF。
因此,我在expression、monomial和factor类里新建了一个去掉多余括号方法的类,根据入栈出栈的思路,让i由0开始移动,直到移动到可以完全去掉的左括号那里,对称的去掉输入字符串的两边,再进行处理。
(2)优化错误
不优化一时爽,一直不优化一直爽,本次作业因为嵌套过于复杂,为了避免错误,每一项输出时都最好在外面增加“()”,在处理优化时,根据前面入栈出栈的思路,仅能处理整个结果最外面的多余括号,而无法继续改进。
在通过“+”连接时,如果后面为空字符串,则不加“+”
本来试图对结果字符串进行化简处理,如果遇到“^1”、“*1”和“*0”则进行化简处理,但却在遇到“*100”和“*002”时遇到错误,受时间关系没有办法成功处理,最后只能遗憾地放弃。
三、代码分析
根据参考往届学长们的博客,可知三个总结程序结构的度量参数具体含义如下:
(1)ev(G):基本复杂度,用来衡量程序非结构化程度的,范围在[1,v(G)]之间,值越大则程序的结构越“病态”。非结构成分降低了程序的质量,增加了代码的维护难度。
(2)Iv(G):模块设计复杂度,用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。
(3)v(G): 循环复杂度,用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数。
参考博客链接:https://www.cnblogs.com/qianmianyu/p/8698557.html
第一次作业
(1)类图
(2)方法度量
(3)分析
1.类图分析
第一次作业时只构建了一个类,运用了多个方法,通过不同的方法来处理输入并输出导数。使得这个类的长度十分的长,结构也不清晰,缺乏面向对象的思维。改进方法:建造两个类,分别处理多项式和单项。多项式类完成对输入字符串判断合法性、接受每个项的求导结果并组合在一起打印出去的功能,单项类的功能是接受输入的单项并提取出系数、指数进行求导、之后将导数结果返回给多项式类。其中系数和指数可以通过二元组共同储存,方便对应关系进行合并提取。
2.复杂度分析
根据结构分析可知,由于将所有的功能处理都放在了main类里,导致main的各个复杂度都十分高,显得结构十分不合理,将其功能分成另外两个类来处理效果会好一点。同时在对合法性判断时,基本复杂度较高,原因在于出现了大量的if/else和while循环,可以考虑研讨课时大佬提出的独占性匹配,使划分更加准确。求导的模块设计复杂度比较高,由于多次调用了其他方法,耦合度较高,可以考虑分类改进后,调用方法数减少来减低复杂度。
第二次作业
(1)类图
(2)方法度量
(3)分析
1.类图分析
这一次的作业开始尝试使用面向对象的思维,构建多个类,结构相较于第一次作业更加的清晰,分别处理多项式、单项式求导、对结果的合并操作以及最后的判断合法性。但缺点也十分突出,依旧没有将输入用类的形式实例化,由此复用性很差,当出现更复杂的表达式(例如第三次)时,便需要重构。
2.复杂度分析
这次的代码,基本复杂度相对来说都不太高,程序模块化和结构化方面做得比上一次作业好得多,而有几个方法的模块复杂度较高,在其中依旧还沿用了面向过程的思想,反复调用一些过程性的方法。而在圈复杂度方面,依旧还是if/else和while循环嵌套层数和个数较多,还是可以巧妙改善正则表达式的匹配模式进行优化代码,使划分更加干净利落。
(1)类图
(2)方法度量
(3)分析
1. 类图分析
本次作业的结构更加优于第二次作业。由于递归的需求,层次之间需要更分明的划分,因此这次的代码结构十分清晰,但遗憾的是,由于时间关系没有用更好的建立公共因子类并采用继承-接口形式,可能结构会更加清晰好看。
2. 复杂度分析
这一次的代码复杂度相比于前两次都高了很多,其主要原因是while循环,if/else式的结构出现次数更多,以及由于递归不同类之间反复调用传递参数进行求导。
四、to hack or not to hack
三次作业的互测环节,均是拿一些自己构造的样例对某一个或某两个人的代码进行检测,遇到错误则提交。尽量保持hack的次数不要太多。
第一次作业
构造样例时,优先考虑了首项出现符号或空格错误、合并同类项后约掉的情况、空串、会出现爆栈的情况、判断/f或/v以及在所有可能加空格的地方随机插入空格的错误。在判断计算是否正确的情况中,构造一些系数或指数较大且含复杂正负号及前置0的情况。
第二次作业
样例构造方法与第一次类似,同时加入对sin(x)哪里可以加空格哪里不能加空格的错误样例。
第三次作业
这次作业较为复杂,可根据自己写代码时遇到的bug来检验其他人的代码。
五、感想
面向对象思维真的特别重要!×3,无论什么时候,在java语言里巧妙地通过类架构可以让代码变得结构清晰易懂,同一接口的类中的同一方法均可被无差别调用,大大的简化了重复的步骤,使代码更加简洁明了。同时,当发现自己的代码结构混乱时,一定不要急于完成,理清好思路再重新构建,否则将会面对代码像马蜂窝一样,debug时常常是改了这点动了那点,触一发而动其身。