3.3.3 表示表
在第二章中,当我们学习了表示集合的各种方式时,我们提到了2.3.3部分中的
维护一个由标识键进行索引的记录表的任务。在2.4.3部分中面向数据的编程的实现中,
我们进行了二维表的扩展使用,使用两个索引键,它的信息被存储和检索。 这里,我们
看一看使用可交互的列表结构如何构建表格。
我们首先考虑一个一维表格,在一个单一的键的情况下,任何一个值都能被存储。
我们实现这个表格用一个记录的列表,任何一条记录都是一个由一个键和一个值组成的数对来实现的。
通过数对记录被粘连在一起形成一个列表。列表的car操作指向连续的记录。
这些粘连用的数对被称为表格的脊柱。当我们把一个新的记录加入到表格时,为了有一个
我们能修改的地方,我们构建了表格为一个有头部的列表。一个有头部的列表在开始处,
有一个特别的主要的数对,它有一个空的记录,在这个例子中,随意的符号是*表格*。
图3。22显示了如下的表的盒与指针图:
a:1
b:2
c:3
图3.22 一个表格表示为一个有头部的列表
为了从一个表格中抽取信息,我们使用lookup程序,它以一个键为参数,返回相关的值
(在这个键下没有记录,返回假)
lookup程序的定义使用了assoc操作,它以一个键和记录的列表为参数.注意的是 assoc
没有看到空的记录。assoc返回给定的键的记录作为它的 car。
lookup然后检查被assoc返回的结果记录是否为真,然后记录的值。
(define (lookup key table)
(let ((record (assoc key (cdr table))))
(if record
(cdr record)
false))
)
(define (assoc key records)
(cond ((null? records) false)
((equal? key (caar records)) (car records))
(else (assoc key (cdr records))))
)
在特定的键值下,为了把一个值,插入到表格中,我们首先使用
assoc来看看是否在表格中已经有了这个键的记录.如果没有,我们
通过组装键和值形成一个新的记录,如果已经存在了这个键值的记录,
用这个新的值来设置记录的cdr部分.为了插入新的记录,表格的头部提供
了一个修改的固定的位置.
(define (insert! key value table)
(let ((record (assoc key (cdr table))))
(if record
(set-cdr! record value)
(set-cdr! table (cons (cons key value) (cdr able))))
)
'ok
)
为了组装一个新的表格,我们简单地创建了一个包含了符号*table*的列表:
(define (make-table)
(list '*table*)
)
*二维的表格
在一个二维的表格中,任何一个值都被两个键索引.我们能组装这样的
一个表格,以一个一维表格,它的任何一个键标识着一个子表格.图3.23
显示了表格的盒与指针图:
math:
+: 43
-: 45
*: 42
letters:
a: 97
b: 98
它有两个子表格.(子表格不需要一个特别的头符号,
因为键标识着子表格,起了这个作用)
图3.23一个两维的表格
当我们查找一个表格中的项时,我们使用第一个键来标识出正确的子表格.
然后我们使用第二个键来标识出子表格中的记录.
(define (lookup key-1 key-2 table)
(let ((subtable (assoc key-1 (cdr table))))
(if subtable
(let ((record (assoc key-2 (cdr subtable))))
(if record
(cdr record)
false))
false
)
)
)
在键的一个数对下,为了插入一个新的项,我们使用assoc来看看在第一个键下
是否已经有一个子表格存在了.如果没有,我们构建一个新的子表包括单独的
一条记录(key-2,value)并且插入到表格的第一个键中.如果针对第一个键的子表格
存在,我们插入新的记录到这个子表格中,使用如上描述的一维表格的插入方法:
(define (insert! key-1 key-2 value table)
(let ((subtable (assoc key-1 (cdr table))))
(if subtable
(let ((record (assoc key-2 (cdr subtable))))
(if record
(set-cdr! record value)
(set-cdr! subtable (cons (cons key-2 value)
(cdr subtable)))))
(set-cdr! table (cons (list key-1 (cons key-2 value))
(cdr table)))
)
)
'ok
)
*创建局部的表格
lookup 和insert!操作的如上定义都以一个表作为一个实际参数.这让我们能够使用程序
读取超过一个表格.处理多个表的另一种方式是对于任何一个表都有自己的独立的
lookup 和insert!程序。我们能使用程序化的表示一个表来实现这一点,正如一个对象维护着
一个内部的表作为它的局部状态的一部分。当发送一个合适的消息,这个表对象提供相关的程序
来操作内部的表。这是一个以这个时尚的方式表示的两维表的生成器:
(define (make-table)
(let ((local-table (list '*table*)))
(define (lookup key-1 key-2 table)
(let ((subtable (assoc key-1 (cdr table))))
(if subtable
(let ((record (assoc key-2 (cdr subtable))))
(if record
(cdr record)
false))
false
)
))
(define (insert! key-1 key-2 value table)
(let ((subtable (assoc key-1 (cdr table))))
(if subtable
(let ((record (assoc key-2 (cdr subtable))))
(if record
(set-cdr! record value)
(set-cdr! subtable (cons (cons key-2 value)
(cdr subtable)))))
(set-cdr! table (cons (list key-1 (cons key-2 value))
(cdr table)))
)
)
'ok)
(define (dispatch m)
(cond
((eq? m 'lookup-proc!) lookup)
((eq? m 'insert-proc!) insert!)
(else (error "Unknown operaion --- TABLE" m))
))
dispatch)
)
使用make-table,我们能实现 get和 put的操作,这些操作在2.4.3部分中的面向数据
编程中被使用过了。如下:
(define operation (make-table))
(define get (operaion-table 'lookup-proc))
(define put (operaion-table 'insert-proc))
get以两个键为参数, put以两个键和一个值为参数。
这些操作都读取相同的局部表,它能被通过调用程序make-table
所创建的对象进行了封装。
练习3.24
在表的如上的实现中,键被测试相同性,使用了equal?(在调用程序assoc时)。
这并不是总是有效的合适的测试。例如,我们有一个表多个键,
我们并不需要精确的匹配我们所查找的数,但是仅有一些误差是可以的。
设计一个表的组装子,它以一个same-key?程序为实际参数,这个程序被用来测试
键的相等性。make-table 应该能返回dispatch程序,它能为一个局部表读取到
合适的lookup 和insert!程序。
练习3.25
泛化一维表格和二维表格,显示如何实现一个表格,值被存储在任意多个键之下,
不同的值可能存储在不同的数量的键之下.为了读取表格,程序lookup 和insert!
应该以一个键的列表为输入参数。
练习3.26
为了搜索表格,正如如上的实现,它需要扫描记录的列表。这是在2.3.3 部分中最基本
的非排序的列表的表示法。对于大的表来说,以一种不同的方式对表进行结构化,可能
是更有效率的。描述一个表的实现,记录被组织为一个二叉树,假定键能够以某种方式
排序(例如,以数字的,或者是字母的)(比较第二章的练习2.66)
练习3.27
识记(也叫做制表)是一种技术,让一个程序能记录,
把被事先已经计算好的值存入局部表中。这种技术能让
一个程序的性能有极大的不同。一个有记性的程序维护着一个表,
之前的调用的值被存储,使用以实际参数为键,来产生值。当一个
有记性的程序被要求计算一个值时,它首先检查表,看是否这个值
已经有了,如果是,返回这个值,如果没有,它以常规的方式计算
这个值并且存入表中。作为制表的一个例子,从1.2.2部分中的计算
斐波那些数的指数过程的复述如下:
(define (fib n)
(cond ((= n 0) 0)
((= n 1 ) 1)
(else (+ (fib (- n 1))
(fib (- n 2))
)
)
)
)
同一个程序的有记性的版本如下:
(define (memo-fib n)
(memoize (lambda (n)
(cond ((= n 0) 0)
((= n 1 ) 1)
(else (+ (memo-fib (- n 1))
(memo-fib (- n 2))
)
)
)
)
)
)
而记忆子被定义为如下的样子:
(define (memoize f)
(let ((table (make-table)))
(lambda (x)
(let ((previously-computed-result (lookup x table)))
(or previously-computed-result
(let ((result (f x)))
(insert! x result table)
result)))
)
)
)
画一个环境图,分析(memo-fib 3)的计算。解释为什么memo-fib
在计算第N个斐波那些数以正比于N的步骤数。如果我们简单地把
memo-fib定义成(memoize fib),这个模式仍然能有效吗?