OO 2022 第一单元(表达式变换)思路分析
去年的作业要求是求导,今年题目换成拆括号+内联自定义函数了。
补充一种今年想到的基于表达式树的表达式存储架构(去年本人的作业采用的是语法分析树,比今年想到的这个表达式树要复杂一些)。
架构示意
在此表达式树存储的基础上, 我们可以很方便地进行拆括号与化简操作。
在进入正题之前,首先补充一些基本设定:
- 在这个表达式树中是不存在减法的。如果要存储
a-b,则以a + -1*b的形式存储,为减数b添加一个系数-1。 - 每一种树节点均应实现
equals和hashCode这两个方法,其作用主要有以下两点:- 判断两个项是否是同类项,需要比较其非系数部分是否完全相等
- 能够使表达式树对象被装入
HashSet或者作为HashMap的 Key。
- 加法节点代表累加,乘法节点代表累乘,这两种节点内部都需要用容器来管理其子节点。
- 相比于有序的容器
List,本人更加倾向使用无序的容器Set或Map(例如HashSet),因为加法和乘法都满足交换律,顺序不重要。 - 使用
HashSet存储子节点时,同一个累加运算下不能出现两个完全相同的项,如出现了,则将即将加入的项乘以2加入,并从HashSet中删除原有的旧元素。
例如x+x,当准备添加第二个x时,HashSet中已经有了一个x,此时将旧的x删掉,并创建一个2*x加入HashSet中。对于乘法同理,sin(x)*sin(x)将以sin(x)**2存储。但x+2*x可以存在,因为x和2*x是两个不同的项,可以在同一个加法运算节点的HashSet下共存。
- 相比于有序的容器
括号拆解
首先说拆括号,在暂不考虑函数的前提下,必须拆括号的情形主要有以下几种:
(a+b)+c, 即一个累加节点的子节点是累加节点。解决方案: 这种表达式树的累加和累乘节点均有多个子节点,其内部用容器来管理子节点(推荐用Set或Map而不是List,因为加法和乘法满足交换律,顺序不重要)。我们只需让累加和累乘节点实现合并操作(类似于 Java 容器的addAll方法),将a和b并到上层,也就的到了a+b+c(a*b)*c, 即累乘节点有子节点是累乘。解决方案同上。(a+b)*c, 即累乘节点有子节点是累加。解决方案: 这时需要利用乘法分配律,循环遍历累加节点下的子节点,并且对累加运算的每个项,复制一份累乘节点下其他的因子并与之相乘,最后将这些相乘的结果相加,即得到a*b + a*c。(...)**n, 乘幂节点的底数是累加或累乘。解决方案: 与乘法分配律类似,多次利用乘法分配律。
化简
接下来讨论化简。主要考虑的化简是合并同类项以及乘幂的折叠等。
在开始化简之前,我们需要做这样的一个准备工作: 为表达式树的每种节点实现 equals 和 hashCode 方法。这两个方法的含义不在此赘述,只需要确保实现之后,我们可以判断两棵树是相等的(例如两个 x*sin(x)*cos(x**2) 相等,更进一步, x**2+2*x+1 和 1+x**2+x*2 这两棵树也应该是相等的,因为加法和乘法顺序不重要)
有了判断两棵树相等的能力(以及将树存入 HashSet 或者作为 HashMap 的 key 的能力)后,就可以正式开始实现化简操作了。这里以合并同类项距离,乘幂折叠的操作与合并同类项相似。
同类项合并
例如 2*sin(x**2)*x**3 + 3*x**3*sin(x**2) 应化简为 5*sin(x**2)*x**3。所谓同类项,也就是除了常数的系数以外,其余含有字母的部分是相同的。
首先,我们需要能够判断出来什么样的两个表达式是同类项,也就是将任何一棵表达式树表示成 系数 * 其他部分 的形式。因此,我们可以对每一种表达式树节点实现 查询系数 和 查询非系数部分 两个方法(注意我们并不需要真正把系数存下来,只需要能查询即可),其中 查询系数 返回一个整数,而 查询非系数部分 返回一棵表达式树。
- 累加节点: 系数为 1,非系数部分为自身。例如
a+b等价于1*(a+b)。注意,两个累加节点不可以合并同类项,否则会产生新的括号。但是可以将它们合并到一起。例如a+b + a+b合并为a+b+a+b。 - 累乘节点: 遍历存储其子节点的容器, 将所有的常数节点提取出来,计算其乘积作为系数。而剩下的节点用一个新的累乘号连接起来就是非系数部分。
- 常数节点: 系数是自身, 非系数部分无。
- 三角函数、变量等: 系数为 1,非系数部分为自身。
对于 Java 中遍历容器的操作,可以尝试采用
stream简化代码。
实现了以上两个查询方法后,我们就可以对一个累加节点的子节点们进行合并同类项:
- 创建一个
HashMap<Node, Integer>,Key 为代表非系数部分的表达式树,Value 为其对应的系数之和。 - 依次遍历累加节点的每个子节点, 分离其系数和非系数部分,并更新到上述
HashMap中(类似于单词计数)。 - 最后遍历上述
HashMap,将每一组系数 * 非系数部分用一个新的累加节点连接起来,得到合并同类项后的结果。
特殊系数的处理:- 系数为
0: 直接丢弃该项 - 系数为
1: 直接保留非系数部分而无需添加多余的乘法。
- 系数为
乘幂折叠
乘幂折叠与合并同类项在操作上相近,只不过操作是在累乘节点上进行,将底数相同的因子合并,指数相加。
输出
最后是表达式树的输出,对每种节点实现 toString 方法,递归遍历输出即可。在输出时可以针对特殊系数(例如 0, 1, -1 等)以及 x**2 -> x*x 等特殊情形进行特判以减少输出长度。实现累加节点的 toString 方法(也就是输出若干项之和)时,如存在至少一个正系数的项,可将该正项最先输出从而少输出一个 - 号。
自定义函数
另外,今年的作业中新增了自定义函数,先输入函数定义然后需要把表达式中的函数调用展开。这里也提供一种基于上述表达式树的参考思路:

浙公网安备 33010602011771号