这是最近做的一个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.对于重复表达式没有共享而是一一计算,没有优化。
但是毕竟这是一个轻量级项目,所以不准备加入这些特性了。