本单元的任务为求导。
即将一个含自变量x的多项式F求导成为另外一个含自变量x的多项式f。使得 dF/dx = f
为降低我们的难度,这个任务被分解成了三个阶段:
(1)对幂函数进行求导(不允许嵌套)
(2)对幂函数和三角函数进行求导(不允许嵌套,三角函数中只能有x)
(3)对幂函数和三角函数进行求导(允许嵌套,三角函数中只能有因子)
一、聊聊思路
1、字符串处理:
在第一和第二个阶段中,对输入的处理时相对比较容易的,因为我们可以使用正则表达式对整个输入字符串进行匹配。
以第一阶段为例,我们可以将每个项分为三种情况 数字、x^n、a*x^n。这里的n可以为任何值,甚至可以在等于1的时候直接省略。
那么这三种情况可以分别匹配两个正则表达式:“[+-]?[0-9]+”,“([+-]?[0-9]?\\s*\\*\\s*)[x]\\s*(^\\s*[+-][0-9]?)?”,第二个正则表达式匹配后三种情况(了解更多关于正则表达式)
但是在第三阶段中,由于嵌套的出现,除非在处理过程中做非常严格的限制将正则表达式分析的范围进行缩减,否则正则表达式无法完成对函数嵌套的字符串处理。
比如对于以下文法:
A::=aAb|i
那么我们理所当然的写正则表达式A="i",A="a"+A+"b"。在这两条正则表达式的字符串匹配完了之后,我们发现其实A里面并不包含着嵌套的内容,它仅仅只能匹配"aib",而不能匹配“aaibb”。当然,这是一个非常简单的例子,可以用其他的方法来解决这个问题。然而任务中的情况比这个要复杂的多,举这个例子也只是为了阐明在这个阶段不适合使用正则表达式罢了。
这个时候应该用上另外一项利器,词法分析。在计算机编译程序中,通常就是使用词法分析对输入程序进行处理。(了解更多关于词法分析)
2、求导处理:
由于在前一个部分中,三个阶段使用了两种不同的方法对字符串进行处理,在这个部分依旧分开讲这个问题。
在1-2次任务中,形式和所需存储的内容还是相对单一的,第一次任务每个项可以写成 a*x^b 的形式,也就是说我们可以只存储a和b 即可。而第二次任务中,每个项可以写成“a*x^b*sin(x)^c*cos(x)^d”的形式。那么我们需要存储a,b,c,d即可。我们只需要一次取一项,然后提取出对应的参数,然后按照规则求导,然后将参数组返回,这样可以完成求导。
在第三次任务中,形式变得相当的复杂,三角函数中可以塞入项,函数相互嵌套等情况,从而让我们不能用有限的参数表示一个项。上面的方法已经不适用了。
所以在求导时只能做这样的处理:我们按求导类型,分为expression,item,factor。expression可以由多个item加减获得,每个item可以由factor相乘获得。每个factor由三角函数,幂函数,常数,或者嵌套函数(函数+factor)组成。每一个类别都向上一层传递自己读了字符串的什么内容,自己对这段内容的求导结果是什么。每一层在接受自己的下一层传输的信息的同时,也要对信息按照规则进行整合。在最底层的factor中对sin、cos、幂函数应该有本质性的处理方式(比如sin->cos之类的)。
二、程序分析
(1)基于度量来分析自己的程序结构
废话不多说,直接上三次作业的度量数据以及类图。
重要符号意义说明:
ev(G)基本复杂度是用来衡量程序非结构化程度的.
Iv(G)模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。
v(G)是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数。
LOC: Line of Code
NCLOC:Non-Commented Line Of Code
P1
LOC
NCLOC
Entry
90
85
Main
10
5
PolyDerivate
239
217
P2
LOC
NCLOC
Entry
96
86
Main
10
5
Poly
330
288
Term
156
146
P3
LOC
NCLOC
Compact
14
11
Entry
28
24
Expression
32
32
Factor
181
170
Filter
86
77
Item
115
108
Main
14
14
Reader
88
83
②类图(使用工具是intellij(旗舰版)自带的diagram)
P1
P2
P3
缺点:其实很容易看出来,每一次作业的后半部分都有非常大的改动,主要是自己的程序并没有考虑那么复杂的应用,也就是需要什么就写什么。在后面的任务中,几乎要全部重构。
优点:在最前面,Main后面一直是调用Entry。这里的Entry是用与放置不同的使用模式(release,debug,batch_test)。在进行测试的时候大大的方便了自己。
(2)分析自己程序的bug
说实话,自己写出了不少的bug,主要的原因是没有进行足够严格的测试。而且在一些细节问题上没有想清楚导致出现一些小错误,e.g.正负号写反,没有考虑0之类的。
我总结了一下,我犯的错误很多都是在细节实现时反复更改实现方式,从而导致在更改实现方式时,另外一部分的代码的处理结果与另一部分代码需要的函数输入不匹配,导致bug出现。
(3)分析自己发现别人程序bug所采用的策略
虽然我没有参加互测,但是我还是想聊一聊bug查找的一些bug的方法(白盒和黑盒测试)。
白盒测试:是通过程序的源代码进行测试而不使用用户界面。这种类型的测试需要从代码句法发现内部代码在算法,溢出,路径,条件等等中的缺点或者错误,进而加以修正。
这个需要你去逐行阅读代码,同时,要尝试设计测试样例去覆盖程序中的所有的分支。而且你也可以顺便检查一下代码逻辑。
黑盒测试:是通过使用整个软件或某种软件功能来严格地测试, 而并没有通过检查程序的源代码或者很清楚地了解该软件的源代码程序具体是怎样设计的。
直白来说就是,就是知道已有的需求限制,划分等价类进行测试的方法。e.g. 如果只允许输入0-100的数字,那么我么可以划分为以下等价类:非法字符输入;<0; >100; 0-50; 50-100 共5个类型进行测试。对于具体的问题需要具体分析。这可以在宏观层面上发现迅速发现bug,而不需要阅读任何代码。
(4)Applying Creational Pattern
在我的观点看来,助教第一、第二次的目的达到了:让我们习惯面向对象的方法和面向对象的程序编写。
但是第三次的题目,目的应该没有达到:使用继承和接口。在这次作业中,更加核心的东西应该是(文法分析,单例化等)。3个类之间除了2个private变量名和3个函数名相同之外,几乎没有什么共同之处。无论是解析、求导、化简都不一样,在这次作业中继承和借口的使用的急迫程度依旧不存在。