[ 目次, 前節, 次節, 索引 ]

3.3.3 表の表現



2章で集合のいろいろな表現を学んだ時, 2.3.3節で, 識別用キーで指標をつけたレコードの表を保守する話をした. 2.4.3節のデータ主導プログラミングの実装では, 情報を二つのキーを使って格納し, 検索する, 二次元の表を多用した. ここでは可変リスト構造として表を作る方法を見よう.

   まずそれぞれの値が一つのキーで格納される一次元の表を考える. 表をレコードのリストとして実装し, 各レコードはキーと対応づけられた値からなる対として実装する. レコードは, carが順次のレコードを指している対で糊づけされたリストとなる. 糊づけした対は表の 背骨(backbone)という. 新しいレコードを表に追加する時に, 変更する場所が必要なので, 表を 頭つきリスト(headed list)にする. 頭つきリストには先頭に特別な背骨の対があり, ダミーのレコード---今の場合は任意に選んだ記号 *table*---を持っている. 図3.22は表

a:  1
b:  2
c:  3
の箱とポインタ図を示す.



図3.22 頭つきリストとしての表の表現

   表から情報を取り出すには, 引数としてキーをとり, 対応づけられた値を返す(または, そのキーで格納した値がなければ, 偽となる.)lookup手続きを使う. lookup手続きは, 引数としてキーとレコードのリストをとるassoc演算を使って定義する. assocはダミーのレコードを見ないことに注意しよう. assoccarに与えられたキーを持つレコードを返す.~24 lookupassocが返した結果のレコードが偽でないことを見, レコードの値(cdr)を返す.


(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を使い, 表に既にこのレコードが存在するかを見る. なければ, キーと値をconsして新しいレコードを作り, これを表のレコードのリストの先頭にあるダミーのレコードの後に挿入する. このキーのレコードが既に存在していれば, そのレコードの cdrを, 指示した新しい値に設定する. 表の先頭は新しいレコードを挿入するために修正する固定の場所を用意する.25


(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 table)))))
  '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)
局所表の作り方
上で定義したlookupinsert!演算は引数として表をとる. それ故, 複数の表にアクセスするプログラムを使うことが出来る. 複数の表を扱うもう一つの方法は, 各表に別々のlookupinsert!を作ることである. それには表を手続きとして表現する,つまり内部の表を局所状態の一部として保持するオブジェクトにする. 適切なメッセージを送ると,この「表オブジェクト」は, 内部の表に操作する手続きをくれる. この方式で表現した二次元の表の生成プログラムがある:

(define (make-table)
  (let ((local-table (list '*table*)))
    (define (lookup key-1 key-2)
      (let ((subtable (assoc key-1 (cdr local-table))))
        (if subtable
            (let ((record (assoc key-2 (cdr subtable))))
              (if record
                  (cdr record)
                  false))
            false)))
    (define (insert! key-1 key-2 value)
      (let ((subtable (assoc key-1 (cdr local-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! local-table
                      (cons (list key-1
                                  (cons key-2 value))
                            (cdr local-table)))))
      'ok)    
    (define (dispatch m)
      (cond ((eq? m 'lookup-proc) lookup)
            ((eq? m 'insert-proc!) insert!)
            (else (error "Unknown operation -- TABLE" m))))
    dispatch))

   make-tableを使うと, 2.4.3節のデータ主導プログラミングで使ったgetputの演算を次のように実装出来る:

(define operation-table (make-table))
(define get (operation-table 'lookup-proc))
(define put (operation-table 'insert-proc!))
getは引数として二つのキーを, putは引数として二つのキーと一つの値をとる. どちらの演算も, make-tableの呼出しが作り出したオブジェクトにカプセル化された, 同じ局所表にアクセスする.

問題 3.24


表の上の実装では, キーはその等価性を(assocの呼び出す) equal?を使ってテストする. これは常に適切なテストとは限らない. 例えば数値のキーを持つ表があり,探している数値と厳密に一致する必要はなく, ある許容範囲内にあればよいかも知れない. 表の構成子make-tableで引数としてキーの等価性のテストに使うsame-key?手続きをとるものを設計せよ. make-tableは, 局所表の適切なlookupinsert!の手続きへのアクセスに使うdispatch手続きを返すものとする.

問題 3.25


一次元, 二次元を一般化し, 値が任意個数のキーで格納され, 値が異ればキーの個数も異るかも知れぬ表を実装する方法を示せ. lookup insert! の手続きは, 入力として表にアクセスするのに使うキーのリストをとるものとする.

問題 3.26


上で実装した表を探すのに, レコードのリストを走査する必要がある. これは基本的に2.3.3節の順序づけられていないリスト表現である. 巨大な表では, 別のやり方で表を構成する方がもっと効率的かも知れない. (キー, 値)のレコードが二進木を使って組織化する表の実装を述べよ. キーは何らかの方法で(数値的にかアルファベット順かで)順序づけられるものと仮定する. (2章の問題2.66と比べよ.)

問題 3.27


メモ化(memoization)(またはテーブル化(tabulation))は手続きに以前計算した値を局所表に記録させる技法である. この技法はプログラムの効率に大きな差をつけることがある. メモ化の手続きは表を維持し, そこに過去の呼出しの値を, その値を生じた引数をキーとして格納する. メモ化手続きが値を計算するよう依頼されると, まずその値がすでにそこに存在するか表を調べ, それがあればその値を返す. それがなければ通常の方法で新しい値を計算し, これを表に格納する. メモ化の例として, 1.2.2節のFibonacci数を計算する指数的プロセスを思い出そう:
(define (fib n)
  (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (fib (- n 1))
                 (fib (- n 2))))))
同じ手続きのメモ化版は

(define memo-fib
  (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-fibn番目のFibonacci数をnに比例したステップ数で計算出来る理由を説明せよ. この方式はmemo-fibを単に(memoize fib)と定義してもやはり働くだろうか.



24 assocequal?を使うので, キーが記号, 数値,リスト構造のいずれでも判別する.

25 背骨の最初の対は, 表「自身」を表現するオブジェクトである; つまり表へのポインタはこの対へのポインタである. この背骨の対は, 常に表の先頭である. 表をこのように配置しないと, insert!は新しいレコードを追加すると, 表の先頭としての新しい値を返さなければならない.

[ 目次, 前節, 次節, 索引 ]