当看完了书中关于符号化求导的例子之后,做到习题2.58的时候,发现此题之中 b) 小题要求实现的标准代数式写法的求导过程确实有一定的难度。当然,如果题目不难的话就不是SICP的风格了,哈哈。
当我仔细分析题意的时候发现,如果确实需要按照题目中所提示的方法,通过改写构造函数与选择函数来实现的话,那就先要去实现 a) 小题中的中缀表达式的构造与选择过程,然后再通过类比上一题2.57的实现思路,确实可以写出来。但是我感觉太麻烦了,如果再让你去实现后缀表达式的求导呢?难道又要改写出一套后缀表达式的构造与选择过程来?(当然,作者在这里没有要求我们这么做。)
作为一种新思路,想要把我们已经实现了的符号化求导过程库能运用到各种类型的表达式上,唯一的一劳永逸的解决方案应该是对被求导的表达式做预处理。这是一种对数据先进行预处理的思想:
打个比方,你手头有一个只能播放 .avi 格式的MP4播放器,但是别人又给你了一部苍老师的 .rmvb 格式的爱情动作片,你在不打算重新买一个新的MP4播放器的前提下,唯一合理的选择就是将这部 .rmvb 格式的教育片转化成.avi 格式的。这就是进行数据预处理的思想。
回到题目中,先前书中的例子实现的符号化求导程序都是基于前缀表达式的,那如果我们可以吧中缀的表达式先给他预处理成前缀的表达式,那么先前的符号化求导程序就一句话都不用改了,然后我们再把求得的结果给转化回中缀表达式,那么题目所要求实现的目的也就自然达到了。
因此,问题的重点就变成了如何在中缀表达式与前缀表达式之间能进行来回的转换。关于这个问题,是最基础的有关数据结构的算法了,网上随便一搜就有一大堆,我在以前学习C语言的时候也写过好几次,只要稍微改一改,用scheme的语法再重写一遍就完了,算法基本不变,就是有关于对栈这种数据结构的合理运用。
那就不多啰嗦了,直接上代码吧(所有代码都已经调试通过,关于运行环境在我的第一篇博客中有介绍):
首先是书中和前面的习题中已经实现的对于前缀表达式求导的基础文件
symbolic-derivative.scm:
; 符号化的求导过程
; 可以求和、积、常数、n次幂的导数
; 只能计算前缀的多元和、积表达式
(define (deriv expr var)
(cond ((number? expr) 0)
((variable? expr)
(if (same-variable? expr var) 1 0))
((sum? expr)
(make-sum
(deriv (addend expr) var)
(deriv (augend expr) var)))
((product? expr)
(make-sum
(make-product
(multiplier expr)
(deriv (multiplicand expr) var))
(make-product
(deriv (multiplier expr) var)
(multiplicand expr))))
((exponentiation? expr)
(let ((n (exponent expr))
(u (base expr)))
(make-product
n
(make-product
(make-exponentiation
u
(make-sum n '-1))
(deriv u var)))))
(else
(error "unknown expression type -- DERIV" expr))))
; 判断变量是否是符号
(define (variable? x) (symbol? x))
; 判断两个变量是否相同
(define (same-variable? v1 v2)
(and (variable? v1) (variable? v2) (eq? v1 v2)))
; 判断是否是和式
(define (sum? x)
(and (pair? x) (eq? (car x) '+)))
; 选取被加数
(define (addend s) (cadr s))
; 选取加数------------------------
; 修改后的选择加数过程 使之可以使用于 n 元表达式
(define (augend s)
(if (null? (cdddr s))
(caddr s)
(cons '+ (cddr s))))
; 判断是否是乘式
(define (product? x)
(and (pair? x) (eq? (car x) '*)))
; 选择被乘数
(define (multiplier p) (cadr p))
; 选择乘数------------------------
; 修改后的选择乘数过程 使之可以使用于 n 元表达式
(define (multiplicand p)
(if (null? (cdddr p))
(caddr p)
(cons '* (cddr p))))
; 判断某个表达式是否是一个给定的数
(define (=number? expr num)
(and (number? expr) (= expr num)))
; 构造一个二元和式 并化简
(define (make-sum a1 a2)
(cond ((=number? a1 0) a2)
((=number? a2 0) a1)
((and (number? a1) (number? a2)) (+ a1 a2))
(else (list '+ a1 a2))))
; 构造一个二元乘式 并化简
(define (make-product m1 m2)
(cond ((or (=number? m1 0) (=number? m2 0)) 0)
((=number? m1 1) m2)
((=number? m2 1) m1)
((and (number? m1) (number? m2)) (* m1 m2))
(else (list '* m1 m2))))
; 判断是否是一个n次幂
(define (exponentiation? x)
(and (pair? x) (eq? (car x) '**)))
; 取得n次幂的底数
(define (base x) (cadr x))
; 取得n次幂的指数
(define (exponent x) (caddr x))
; 构造一个n次幂 并化简
(define (make-exponentiation u n)
(cond ((=number? n 0) 1)
((=number? n 1) u)
((=number? u 1) 1)
((and (number? u) (number? n)) (expt u n))
(else (list '** u n))))
然后是对于表达式的转换(预处理),也是最重要的一个文件,解决整个问题的核心所在
expression-conversion.scm:
(load "symbolic-derivative.scm")
; 表达式形式之间的转换:
; 中缀转换成前缀
; 前缀转换成中缀
; 判断是否是操作符
(define (is-op? t)
(or (eq? '+ t)
(eq? '* t)
(eq? '** t)))
; 返回操作符的优先级
(define (order t)
(cond ((eq? '+ t) 1)
((eq? '* t) 2)
((eq? '** t) 3)
(else (error "Unknown symbol!"))))
; 根据操作符返回对应的操作
(define (get-operator op)
(cond ((eq? '+ op) make-sum)
((eq? '* op) make-product)
((eq? '** op) make-exponentiation)
(else (error "Unknown symbol!"))))
; 中缀转换成前缀
(define (infix-to-prefix expr)
; 转换的迭代逻辑
(define (do-iter expr result-stack op-stack)
; 中间结果的计算
(define (calculate expr op a1 a2 remain op-stack)
(do-iter
expr
(cons ((get-operator op) a1 a2) remain)
op-stack))
; 判断是否是一个表达式
; 如果是表达式 则递归执行 do-iter 否则返回本身
(define (is-expression? x)
(if (pair? x)
(do-iter x '() '())
x))
; 表达式是否为空
(if (null? expr)
; 检查符号栈是否为空
(if (null? op-stack)
; 如果符号栈为空 表明计算结束 出栈结果
(car result-stack)
; 如果符号栈非空 表明计算未完成 继续计算
(calculate
expr
(car op-stack)
(cadr result-stack)
(car result-stack)
(cddr result-stack)
(cdr op-stack)))
; 表达式不空 先取出其中的第一个元素 this
(let ((this (car expr))
(other (cdr expr)))
; 第一个元素是否是操作符
(if (is-op? this)
; 如果是操作符 检查符号栈是否为空
(if (null? op-stack)
; 如果空 直接将其压栈
(do-iter other result-stack (cons this op-stack))
; 否则与栈顶元素比较优先级
(let ((op-top (car op-stack))
(res-top (car result-stack)))
; 如果不高于栈顶元素优先级
(if (>= (order op-top) (order this))
; 弹出结果栈中的两个元素进行计算 并将计算结果再压栈
(calculate
expr
op-top
(cadr result-stack)
res-top
(cddr result-stack)
(cdr op-stack))
; 否则 直接将操作符压栈
; 运算优先级由入栈顺序保证
(do-iter
other
result-stack
(cons this op-stack)))))
; 如果不是操作符
; 将结果都压入栈 result-stack 中
(do-iter
other
(cons
(is-expression? this)
result-stack)
op-stack)))))
; 开始转换表达式
(do-iter expr '() '()))
; 前缀转换成中缀
(define (prefix-to-infix expr)
(define (do-it expr outer-order)
; 是否是一个表达式
(if (pair? expr)
; 如果是前缀表达式 取出操作符
(let ((op (car expr)))
; 得到操作符的优先级 并取出两个操作项目
(let ((this-order (order op))
(a1 (cadr expr))
(a2 (caddr expr)))
; 如果这一层操作的优先级小于外层 就加括号
((if (> outer-order this-order) list (lambda (x) x))
(append
(do-it a1 this-order)
(cons op (do-it a2 this-order))))))
; 不是表达式就直接返回
(list expr)))
; 开始转换 并传入最低的优先级
(do-it expr (order '+)))
; 将一个前缀表达式转换成规范化的中缀表达式
(define (normalize expr)
(prefix-to-infix (infix-to-prefix (prefix-to-infix expr))))
然后我们可以将一个前缀表达式的求导过程分装成一个中缀表达式的求导过程
derivative-infix.scm:
(load "expression-conversion.scm")
; 符号化的求导过程
; 可以求和、积、常数、n次幂的导数
; 能计算中缀的多元和、积表达式
; 结果返回中缀表达式
(define (deriv-infix expr var)
(normalize
(deriv (infix-to-prefix expr) var)))
最后是一些测试的代码
test.scm:
(load "derivative-infix.scm")
(define expr '(x + x + x + (x + 3 * (3 * x ** 3 + y + 2)) + x + x ** 2))
(display expr)
(newline)
(display (deriv-infix expr 'x))
(define expr1 '(x + 3 * (x + y + 2)))
(newline)
(display expr1)
(newline)
(display (deriv-infix expr1 'x))
最后,要说明一下,我在这里是纯粹为了做题目,所以就完全没考虑去检查表达式的合法性,因为如果这样就会额外增加许多的冗余代码出来,使得最重要的转换思想变得难以清晰地被表达出来(scheme本身的可读性就差,要是再写得安全一点根本就不用去读了,但是我还是加入了很多注释,希望可以在以后帮助阅读)。
其实,当运行测试代码的时候会发现,这样做还有一些不尽如人意的地方,比方说计算的结果不会合并同类项等等。程序改进的地方还是有的,但是我不想再做下去了,因为这已经不是主要的部分了。