3.3.3 表示表

3.3.3 表示表
在第二章中,当我们学习了表示集合的各种方式时,我们提到了2.3.3部分中的
维护一个由标识键进行索引的记录表的任务。在2.4.3部分中面向数据的编程的实现中,
我们进行了二维表的扩展使用,使用两个索引键,它的信息被存储和检索。 这里,我们
看一看使用可交互的列表结构如何构建表格。

我们首先考虑一个一维表格,在一个单一的键的情况下,任何一个值都能被存储。
我们实现这个表格用一个记录的列表,任何一条记录都是一个由一个键和一个值组成的数对来实现的。
通过数对记录被粘连在一起形成一个列表。列表的car操作指向连续的记录。
这些粘连用的数对被称为表格的脊柱。当我们把一个新的记录加入到表格时,为了有一个
我们能修改的地方,我们构建了表格为一个有头部的列表。一个有头部的列表在开始处,
有一个特别的主要的数对,它有一个空的记录,在这个例子中,随意的符号是*表格*。
图3。22显示了如下的表的盒与指针图:

a:1
b:2
c:3

翻译连载: 第60篇 计算机编程的结构与解释   3.3.3 表示表 - gggwfn1982 - gggwfn1982的个人主页

图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

它有两个子表格.(子表格不需要一个特别的头符号,
因为键标识着子表格,起了这个作用)

翻译连载: 第60篇 计算机编程的结构与解释   3.3.3 表示表 - gggwfn1982 - gggwfn1982的个人主页

 
图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),这个模式仍然能有效吗?

猜你喜欢

转载自blog.csdn.net/gggwfn1982/article/details/81611016