版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。
求导的符号运算
这是最近做的一个python的小项目,内容是实现求导的符号运算,虽然我最终的实现化简能力能力比较弱,但是还是一个很有意思的项目。

语法树的建立

语法树的结构

语法树是我这个项目里面所有表达式的存在形式。
树的一个结点要么是字符串(常数或者变量),要么是内部结点,内部结点代表一棵子树,对应存储的结构体有两个字段:根存储的运算和子结点的列表,列表元素个数是操作符的元数,列表元素是子结点。
例如前缀表达式(+ (* x 2) 1)表示2x+1,那么对应的树根结点是’+’,对应的子结点列表是[tree(2x), 1],其中tree(2x)又是一棵子语法树,根为’*’, 对应子结点的列表是[2, x]。即语法树是递归定义的,最终叶子结点是常量或者变量。

前缀表达式转化为语法树

我的项目中约定输入前缀表达式,即类似lisp的表达式,所以转化非常方便。
对于前缀表达式(op arg0, arg1, …, argn),先读入op为根结点操作符,然后读入参数列表即可建立语法树,注意读入参数列表过程中可能读到表达式,需要递归建树。

语法树的运算

求值

利用前缀表达式的算法递归求值即可。遇到变量,代入具体数值即可。
设calculate(tree,x)计算tree在变量取值x时候的值。

求函数

因为要求一个函数而不是具体数值,所以需要返回一个函数或者lambda表达式,可以实现func(tree) 为lambda x: calculate(tree, x)。

求导

这里是算法的核心。
对于二元运算,算法为递归求导,规则如下:
1.叶子结点:x导数为1,常数导数为0。
2.非叶子结点,左右子树为l,r。那么结点的运算来递归求导:
(l+r)′=l′+r′
(l−r)′=l′−r′
(l∗r)′=l∗r′+l′∗r
(l/r)′=(l′∗r−l∗r′)/(r2)
(lr)′=(rlr−1)∗l′+ln(l)lr∗r′
手动实现这五条规则即可。
对于一元函数,有统一的计算方法,即链式法则:
h(g(x))′=h′(g(x))∗g′(x)
所以需要保存一个函数表,保存下每一个函数的导函数,利用链式法则递归计算即可。注意我们这儿并不保存导函数的函数形式,而是保存如何建立导函数语法树的lambda表达式。、
例如,ln(x)的导函数就是lambda x: ExpTree(‘\’, [1, x])。

语法树化简

因为上述的求导规则经常会产生0或者1项,例如:
(2∗x)′=(2)′∗x+2∗(x)′=0∗x+2∗1
所以需要化简,规则为:
1.0+M=M+0=M
2.M−0=M
3.1∗M=M∗1=M
4.0∗M=M∗0=0
5.M/1=M
6.0/M=0
7.M1=M
8.0M=0
手动实现规则即可。
要注意的是,需要先化简子树来递归化简。另外,如果左右子树都是常数,可以直接计算。

界面和交互

界面需要用户输入前缀表达式和运算指令,输出对应的函数或者函数值,另外输出对应函数的时候,为了直观,除了函数的中缀表达式,我还输出了函数图像。
交互上我没有花和大力气,所以格式相对比较死板,不过提供了足够的帮助信息,应该用起来很方便。

还需要改进的地方

但是,这毕竟只是一个耗时一天不到的小项目,要改进的地方还有很多:
1.这个项目不能运用运算律和函数性质化简,所以以下这些化简,暂时无法完成:
x-x=0,2x+3x=5x,sinx * sinx + cosx * cosx=1,ln(exp(x))=x。
2.这个项目暂时不支持自己指定中间函数,这个涉及到词法语法分析等等,没有实现。
3.对于重复表达式没有共享而是一一计算,没有优化。
但是毕竟这是一个轻量级项目,所以不准备加入这些特性了。