BUAA_2023_oo_Unit1.3 总结 (新增求导因子)
Part 1 第三次作业 全局理解
第三次作业其实思路非常简单,在我看来,它的本质可以理解为四个字:处理输入。
这四个字可以从以下两个方面理解:(这两个方面几乎是本次作业新增内容的充分必要条件)
输入函数定义式
这一输入新增不同:函数定义式中可以调用其它自定义函数,函数定义式中等号右边可以出现求导算子
输入待去括号表达式
这一输入新增不同:表达式中可以出现求导算子
那么我们显然可以知道,我们只需要将输入预处理(定义函数式展开重复调用、展开求导;输入表达式展开求导)一下,这样处理过后的输入便将和我们第二次作业的输入完全相同,这样的话直接把处理后的输入怼到第二问主体代码中相应的输入端中去,然后剩下的部分不需要做任何改动(优化也与本题的新增条件无关),本次作业便完成了!
如果文字不够直观的话,这一想法还可以用下面的图表示:(核心在粉色五角星所在的粉色边框内)
我们需要认识到下面几点:
-
符合第三次作业的要求的输入,经过预处理,可以完全转换为符合第二次作业的输入。且具体的化简步骤与第二次作业相同。
也就是我们只需要实现粉色框内的橙色长方形,则本次作业即可解决!
-
预处理中,要按照 先
去除fgh这三个函数调用符号(也就是“代入”),再去除求导算子的顺序进行。很容易明白。 -
这种思路不需要考虑指导书中说的""输入有h(x) = dx(x)时,h(sin(x))是先去掉h再算导数还是先算导数再去掉h"这种情况,因为我们预处理后的函数定义式中不再存在求导算子。
下面,我们解决求导、定义函数可调用其他已定义函数这两个问题,这两个问题解决后,我们的输入便可顺利得到预处理。
Part 2 [ 直观理解 ] 递归下降求导
求导是处理自定义函数和输入表达式的共同基础,我们先解决求导问题。
我们一步步地构建出直观递归图
很自然地,我们会想到定义一个方法(不妨起名为derAll),这个方法可以接收一个表达式字符串(我们这里认为是不含函数调用的表达式,当然也可以含,这无关紧要,相差一个函数调用的代入而已)作为输入,输出这个表达式去除其中的求导因子后的字符串。
这个方法的实现思路比较容易想到:
- 找到输入表达式中的
dx(当然,dydz也是,这里都用dx表示) - 将
dx(表达式)这个部分的字符串替换成去除求导因子后的结果 (在这里,我们调用一下我们hw2已经写好的去括号方法对dx内的部分去括号然后再处理,这一步的目的是把一个玩具(表达式)拆卸成最小零部件(最小因子)的集合,这会为我们顺利递归服务)
关键在第二步中的去除求导因子,那么我们便可以再定义一个方法derExpr,这个方法接收两个参数待求导表达式因子,求导目标变量,返回求导结果字符串,那么我们的递归图可进一步延展:

我们知道,表达式是这样的结构:expr = term1 + term2 + ...,那么我们为了对表达式求导,我们只需要识别出表达式的各个项,逐项求导再求和。为了逐项求导,我们定义derTerm方法,能够接收项字符串,求导对象,输出求导结果字符串(这里不需要(当然,多加一层括号也无妨)保证返回的结果为因子,因为这个方法只会在derExpr中被调用,而derExpr最后返回时我们保证因子性)。
同时,项的结构是term = f1 * f2 * ...(f是factor的简写,而非自定义函数名),那么我们对项求导,也就是这样的一个过程(以三个因子为例):dx(term) = 常数(只要不含求导对象就认为是常数) * (dx(f1)*f2*f3 + f1*dx(f2)*f3 +f1*f2*dx(f3)) (对某一因子求导,其他因子都不导,全部加起来)
那么我们又需要能够对因子求导,因此便需要有一个derFactor(对因子求导)的过程,因子有多种,我们需要分类讨论。
这里,可能出现的因子分为:三角函数因子,幂函数因子,表达式因子(常数部分在前面已经提取出,因此不会出现整数因子什么的)。
基于此段,我们将递归图进一步扩展:

我们将幂函数因子用紫色框框住,因为我们不难想到,对幂函数因子求导是递归的尽头!
除了幂函数外,还有:
1、三角函数因子:其内部因子可能出现的类型为——三角函数因子,幂函数因子,表达式因子(不含整数因子,因为在derTerm中我们已经将所有不含自变量的因子剔除出去了)这三种再下层因子,我们都已经讨论过,他们三个的行为分别是:递归调用三角函数求导方法/递归的尽头/递归调用表达式因子方法(递归调用我们用红色的线条表示)
2、表达式因子:对表达式因子的求导我们已经在最开始的时候讨论过了,这里再次出现,便是递归的体现,它需要重新去调用这个图构建的起点部分。(递归调用我们用红色的线条表示)
因此,我们可以对递归图进行扩展:(derTri是解析三角函数因子的方法,derPower是解析幂函数因子的方法)

最后我们可以随心加一些递归尽头(图上紫色部分),即在处理相应的对象时,如果此对象不含求导对象,那么直接返回"0"即可,不需要做无谓的递归了。
再次强调一下三条红线代表的是递归调用。
在构建到这一步时,我们的这张图已经完成了闭环,成功闭环也意味着我们的工作已经完成了。
根据这个图,我们也可以对所谓的递归下降法有一个更深入一些的理解,从这个图的最顶端给一个输入后,程序在运行的过程中,我们关注的焦点是在不断向下走的,也就是下降,即使因为递归调用函数又会回到上面,但是最终的归宿一定是这个图的最底端(递归终止处)(类似于小球经历磕磕绊绊地路途后最终还是会因为重力落到最底端)。
相信明白了这张图后,程序的具体实现就不是什么困难的事情了。
注意,我们应认识到:
-
维护方法调用前后字符串的“因子性”,也就是说当方法接收一个因子时,我们尽量让它返回的字符串也是一个因子类型(可通过返回字符串两边加括号等方式实现),否则便可能出现把返回结果替换后因为不再是因子了,运算优先级导致出现bug的情况。
-
别忘了三角函数因子也是可能有指数部分的
-
derFactor其实只是起一个分类作用,将因子分类,然后让他们去进行对应处理,可以不用实现而在derTerm中完成因子的分类工作(当然,实现了derFactor也没事)
Part 3 自定义函数调用其他已定义函数问题
注意,我们的指导书说只能调用已定义函数,因此对于输入的每一个函数式子的处理,不需要考虑在其后输入的函数。
这个问题很简单,如果我们之前实现过方法deleteFunc(将一个表达式中的函数调用全去掉)的话,只需要对函数表达式先用一下这个方法,再用一下我们上面已经实现了的表达式去除求导因子的方法derAll(这个表达式中不一定有求导因子,没有的话我们的这个方法原形返回就可以了),便完成了处理,变成第二次作业的输入的形式了!
核心代码几乎只有这一句。
Part 4 总结
如果认识到这其实可以理解成一个对输入的转化的问题的话,几乎任何方法都无需重构而只需要将原始输入改成第二次作业的格式作为真正的输入即可。
在实现了上面的方法后,对于两种输入(函数定义,表达式)的处理都是这两个分顺序的步骤:
step 1.用deleteFunc方法将式子中的函数调用去掉。
step 2.用derAll方法将式子中的求导因子去掉。
水平非常有限,如果有不严谨或者是完全错误的地方的话希望大家友好指教,感谢!

浙公网安备 33010602011771号