BUAA OO 第一单元总结

文章讲述了作者在三次编程作业中逐步解决表达式解析、数据存储、计算过程优化等问题,包括使用递归下降算法处理表达式,哈希表存储多项式,以及在后续作业中对结构进行重构以提高效率和避免bug。
摘要由CSDN通过智能技术生成

作业1

作业1要求

读入一个包含加、减、乘、乘方以及括号的单变量表达式,输出恒等变形展开所有括号后的表达式。

基本框架

表达式解析:我运用了递归下降算法,将表达式利用Lexer类和Parser类转化为一个Expression类,而在Expression等因子类和Term类中定义toString方法将每个因子按照后缀表达式的形式输出到一个字符串中,这样整个问题就转化成了计算后缀表达式的问题。在表达式解析过程中Lexer类的prepare方法中我还对原表达式进行了预处理,将连续的加减号变成一个加减号,将乘方符号后可能出现的正号消去,在开头的加减号前加“0”。在Lexer中的next方法中我也对表达式做了另一种“预处理”,即遇到括号中的因子以正负号开始,就增加“-1*”或“1*”,这些预处理的目的都是为了将所有加减号变成运算符,去除表达正负的正负号,以方便后缀表达式的计算

数据存储:每个单项式可以表示成 系数*x^指数 的形式,所以我使用了哈希表来存储一个多项式,Key是每个单项式的指数,Value是每个单项式的系数。当然这种存储方式在第二次作业中遭到了重创,也是之后需要重构的最主要原因。

计算过程与输出:定义了两个哈希表的加减乘乘方运算函数。最后通过change函数将哈希表变成一个字符串输出。

UML类图

在这里插入图片描述

1.MainClass类:该类为主类,执行所有的流程
2.Lexer类:词法分析类,负责部分的预处理和递归下降是提供因子和运算符
3.Parser类:递归下降类,实现递归下降算法
4.Factor类:接口,统一所有因子
5.Expression类:表达式类
6.Term:项类
7.Power:幂函数因子类
8.Number:常数因子类
9.Variable:变量因子类

第一次设计大家应该是大同小异,也就不谈论优缺点了。

基于度量的结构分析

度量指标:
ev(G) 基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。

iv(G) 模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。

v(G) 是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。

在这里插入图片描述

在这里插入图片描述
可以看到在Lexer类的next,prepare方法,主类的take,change方法的的v(G)都比较大,这是因为前者面对不同情况处理某个字符的
方法不同,后者在字符串和哈希表转换时需要面对指数为0,系数为0等特殊情况,所以这些方法都需要大量的if嵌套,导致复杂度偏高。
而iv(G)较大的原因是调用很多StringBuilder类的append方法和String类的charAt方法,我并不认为这有什么问题,因为这些模块并不是我自己建立的,并不需要我维护,所以不需要担心其耦合度高的问题。

bug分析

1.在计算后缀表达式时,我使用了Arraylist来储存变成字符串的多项式,然后每出现一个运算符,就从Arraylist弹出最后多项式来进行运算,将这两个多项式删除,然后将运算的结果重新转化为字符串放入Arraylist中。在这个过程中,我误以为删除的多项式一定就是我弹出的多项式,但实际情况是如果两个多项式生成的字符串完全相同,那么会导致删除的不是最后的字符串,而是第一个,这就导致了误删了应该保留的字符串,产生了bug。
在bug修复时,我首先想到的是既然计算后缀表达式运用的是栈的结构,那么为什么不去用栈的容器呢?(当然我是在同学提醒下才想起还有栈这个容器的)

在这里插入图片描述
这样就解决了之前的问题。但是在仔细思考后,我才发现我设计的计算过程着实有点蠢,在我原来的设计过程中,因为从后缀表达式中得到的是字符串,所以我直接就选择将字符串放入容器中,在之后的运算中,我需要先将字符串转化为多项式哈希表,再进行哈希表之间的运算,最后将新产生的哈希表重新变成字符串,放回容器。这种设计导致浪费了大量的时间,既然我们运算需要的是哈希表,那么为何不直接将每个得到的字符串先转化为哈希表,将哈希表作为栈存放的内容,在计算得到最后的哈希表后,只需要做一次哈希表转化回字符串的算法,即可完成目的。

作业2

作业2要求

新增自定义函数部分以及括号嵌套以及指数函数

基本框架

自定义函数替换:这里我使用了哈希表来存储自定义函数。在得到一个自定义函数后,将如f、g等函数名作为哈希表的Key,将函数的参数依次放入一个Arraylist中,最后将函数表达式放入Arraylist,然后将这个Arraylist作为这个Key对应的Value值。在得到目标表达式后,先遍历,如果哈希表的Key中存在遍历得到的字符,那么对应的Arraylist中的最后就是需要替换的函数表达式。接着目标表达式中该函数的参数替换掉

表达式解析:首先表达式解析的过程基本不变,无非增加了指数函数Exp类。

数据存储:这是这次作业的难点。可以看到第一次作业哈希表的存储方式在这次作业中已经基本无法使用,我们无法在哈希表的基础上增加指数函数的指数这一参数的存储,更别说面对更加复杂的迭代。所以我选择了建立一个单项式类和一个多项式类去存储,因为整个表达式化简得到的多项式是一些形如 系数 * x^指数 *exp(多项式) 的单项式的相加,所以单项式的属性应该是三个,即系数、指数还有一个多项式类,而多项式类的属性就是一个单项式的Arraylist。所以这里使用了一种递归的管理方式,多项式由单项式组成,而单项式又有多项式的属性,这种递归直到单项式的多项式属性为null为止。

UML类图

在这里插入图片描述
相比第一次作业,这次增加了四个类
1.Fun类:用于对输入表达式进行函数代入预处理
2.Exp类:指数函数类
3.Poly:多项式类
4.Mon:单项式类

相比第一次作业,我将计算过程的方法从主类中抽离,使用多项式类去管理计算过程,这样可以让程序的层次结构更清晰。同时,单项式和多项式的递归定义让运算过程也较为方便,只需让运算方法在单项式和多项式中递归定义即可

基于度量的结构分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

相比第一作业,第二次作业并没有增加什么复杂度较高的方法。第二次作业部分方法复杂度较高的原因依旧出现在预处理和输出过程中的多层if嵌套,所以第二次作业的整体结构较为合理

bug分析

这次的bug就较多了,可以说是相当重量级
首先在函数替换过程中:
1.首先函数替换时如果原本自定义函数的参数有“x”,那么我会暴力使用replaceAll方法,将函数表达式中的“x”全部替换成新的参数。但是需要注意,如果在自定义函数的表达式中含有exp()的形式,那么“exp”中的“x”同样会被替换,产生错误。所以我会在每次输入自定义函数后,将函数表达式中非参数的x全部替换成其他字母,如“exp”替换成“emp”,这样就解决了上面的问题

2.但是这样修改依旧会有问题。比如自定义函数是f(y,x)=y+x3,输入函数表达式是f(x2,x3),那么第一次替换后函数表达式变成了x2+x3,那么第二次替换x时,第一次替换上去的x2中的x会被错误替换成x32,导致了bug产生。那么不如直接将输入自定义函数的参数x、y、z直接替换的别的字母,这样就不会导致替换上去的新参数含有之后需要替换的旧参数的情况了。

然后在计算过程中:
因为在合并同类项中,判断是否是同类项的结束条件是单项式的指数函数的指数同时为null。那么就要求在其他运算结束后,指数函数的指数变为0后,必须强制将其置为null,否则将会导致两个同类项可能无法合并

作业3

作业3要求

增加求导因子,自定义函数嵌套

基本框架

这次作业的新增要求,基本的框架没有太大变化。自定义函数嵌套无非是需要每输入一个自定义函数就进行一次函数替换,而增加求导因子也只需要在递归下降时多一个类去管理导数类,然后在多项式类中增加求导运算。

UML类图

和第二次作业几乎完全相同,就简单用idea插件生成图表示一下
在这里插入图片描述
新增类:
Dx类:求导因子类

基于度量的结构分析

这边也不多赘述了,新增的只有一个求导方法
在这里插入图片描述
在这里插入图片描述
可见对结构并没有太大影响

心得体会

在这三次作业中,最让我头疼的一定是第一次作业,尽管从难度来说第二次作业才是最难的,但是第一次作业的这种从0开始设计更让我无从下手。从零开始,意味着会面对很多抉择,如何处理递归下降后得到的表达式,这让我纠结了许久,因为我一直是想用后缀表达式去计算的,但在和同学讨论后我发现几乎没有人采取和我一样的方式,这让我十分犹豫到底该不该坚定我原本的想法。最后,我还是选择了使用后缀表达式去进行计算,后来我发现这种方法也有很大的好处,在面对((((x+x)8)8)8)8的数据点时,我的同学的运算过程是将所有(x+x)进行相乘,导致TLE,而使用后缀表达式进行计算,就会在做完一次(x+x)^8后,用算出的结果再做8次方操作,这样就大大减少了运算时间。所以很多时候,尽管自己的想法和大多数人不同,但也不要轻易放弃,有时候自己的想法虽然在一开始可能不占优势,但是在未来可能会显示出独特的优势。

然后第二次作业的大量重构也很让我头大。重构意味着做取舍,舍去自己原本使用大量精力写的代码是非常需要勇气的,所以我一开始也在想破头皮思考能不能不去重构就完成这次作业呢?在思考后,我也是想到了新的方法,但是我不禁问自己,这次作业的要求是解决了,那么下次呢?面对下次迭代新的要求,再次疯狂思考特殊的解决方法,还不如这次一劳永逸,重构一种适合扩展迭代情况的结构。当然在大段大段删除代码的时候,我感觉我的心在滴血,不过当我用一个多小时拿下第三次作业的时候,我才彻底感受到重构的魅力。

未来方向

感觉第一单元的体验还是不错的,但是我感觉难度的设置还是有点问题。前两次作业我都花了不止一天的时间才能够完成,更别说debug的时间了。但是第三次作业中,加上debug的时间,我也不过花了不到2小时。但这种难度设置产生的原因也无可厚非,第一次作业需要整体结构的设计,第二次作业容易产生大范围的重构,那么第三次作业自然就会比较轻松。那么不如将第三次作业的时间放到第一周,将第一周作业的形式变为报告,来进行对设计思路的设计,然后第二周开始实现代码,第三周再去增加要求,这样感觉难度的增加会比较人性化一点。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值