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

2.3.4 例: Huffman符号化木



本節では集合と木を操作するリスト構造とデータ抽象の使い方を練習する. データを0と1の列(ビット列)で表現する方法へ応用する. 例えば ASCII 標準符号は, 計算機の中でテキストを表現するのに使われ, 各 文字を7ビットの列で符号化する. 7ビットを使うと27個, つまり128個の文字が区別出来る. 通常n個の記号を区別するには, 記号当りlog2nビットが必要である. いま通信文がすべて8個の記号, A, B, C, D, E, F, G, Hで出来ているなら, 1文字当り3ビットの符号を選ぶことが出来る. 例えば:
A 000        C 010        E 100        G 110
B 001        D 011        F 101        H 111
この符号を使うと

BACADAEAFABBAAAGAH

の通信文は54ビットの列

001000010000011000100000101000001001000000000110000111

で符号化される.

   ASCIIや上の AからH 符号のような符号は, 通信文中の各文字を同じビット数で表現するので 固定長(fixed-length)符号という. しかし時には異る記号は異るビット長で表現する 可変長(variable-length)符号を使う利点もある. 例えば モールス符号は, アルファベットの各文字に同数の短点と長点は使っていない. 特に一番よく出てくるEは1個の短点で表現する. 一般に通信文のある記号が頻繁に現れ, 別の記号が殆んど現れなければ, 頻繁な記号に短い符号を割り当てることでデータを更に効率よく(つまり通信文当りのビット数を減らして)符号化出来る. AからHの文字に対する次の符号を考えてみよう.

A 0           C 1010        E 1100        G 1110
B 100         D 1011        F 1101        H 1111
このコードだと, 上と同じ通信文は

100010100101101100011010100100000111001111

で符号化される. これは42ビットで上の固定長の符号に比べ20パーセントの節約になっている.

   可変長符号を使う時の問題点の一つは, 0と1の列を読みながら, 記号の終りに来たことをどう知るかにある. モールス符号はこの問題を各文字の短点と長点の列の後に特別な 分離符号(separator code) (この場合は休止)を置いて解決した. 別の解決法はある記号の符号全体が他の記号の符号の初め(語頭(prefix))にならないように符号を設計する. こういう符号は 語頭符号(prefix code)という. 上の例ではAは0, Bは100と符号化されるので, 他の記号は0や100で始る符号を持ち得ない.

   一般に通信文中の記号の相対頻度を符号化に利用した可変長語頭符号を使うと, かなり節約出来る. このやり方の一つは, 発見者 David Huffmanに従ってHuffman符号化法という. Huffman符号は葉のところに符号化する文字のある 二進木で表現する. 木の, 葉でない節には, その節の下にある葉の記号をすべて含む集合が付随している. 更に葉のところの各文字には重み(相対頻度)が割り当ててあり, 葉でない節にはその下の葉の重みを足し合せた重みが割り当てられている. 重みは符号化, 復号化の時には使わない. 次に木を構成するのに重みがどう役立つかを見よう.

   図2.18は上のAからH符号に対するHuffman木を示す. 葉の重みはこの木が, Aが相対頻度8, Bが3で, 他が1の通信文向けに設計されたことを示している.



図2.18 Huffman符号化木

   Huffman木があれば, ルートから出発しその記号を持つ葉に到達するまで下っていくことで, ある文字の符号が見つかる. 左の枝へ下る度に符号に0を追加し, 右の枝へ下る度に1を追加する. (どちらの枝へ進むかは, どちらの枝にその記号の葉があるか, または集合にその記号を含んでいるかを見て決める.) 例えば図2.18のルートから出発し, Dの葉へは, 右の枝, 左の枝, 右の枝, 右の枝と進む; そこでDの符号は1011である.

   Huffman木を使ってビット列を復号化するには, ルートから始めビット列の0と1の並びを左の枝へ下るか右の枝へ下るか決めるのに使う. 葉に到達する度に, 通信文の新しい記号を作り出し, そこからルートへ戻って次の記号を探し始める. 例えば上の木と, 列10001010が与えられたとする. ルートから出発し, (先頭のビットは1なので)右へ下り, (二番目のビットは0なので)左へ下り, (三番目のビットも0なので)左へ下る. するとBの葉へ到達するから, 符号化通信文の最初の記号はBである. さてもう一度ルートから出発し, 列の次のビットが0だから左へ下る. Aの葉へ到達する. 更にまたルートから残りの列1010を持って出発し, 右, 左, 右, 左と進んでCへ到達する. つまり通信文はBACであった.

Huffman木の生成
記号の「アルファベット」とその相対頻度が与えられた時, 「最良」符号をどう構成したらよいか. (いい替えれば, どういう木が通信文を最小ビット数で符号化するか.) Huffmanはこのためのアルゴリズムを与え, 記号の相対頻度が符号を構成した時の頻度と合っている通信文にとって, 結果の符号は本当に最適の可変長符号であることを示した. ここではHuffman符号の最適性は証明しないが, Huffman木の構成法は示そう.42

   Huffman木の生成アルゴリズムは単純である. 最小頻度の記号をルートから最も遠くに置くという発想である. 符号を構成しようとする初期データから決る記号と頻度からなる葉の節の集合から始める. さて最小の重みを持つ二つの葉を見つけ, 合体させてこの節を左と右に持つ節を作る. 最初の集合からこの二つの節を除去し, 新しい節を置く. この手続きを繰り返す. 毎回最小重みの節を合体させ. 集合から除去し, これらの二つの節を左と右に持つ節で取り替える. 遂に節が一つになったら終了し, これが全体の木のルートになる. 以下が図2.18のHuffman木が生成される様子である:

最初の葉    {(A 8)  (B 3)  (C 1)  (D 1)  (E 1)  (F 1)  (G 1)  (H 1)}
合体        {(A 8)  (B 3)  ({C D} 2)  (E 1)  (F 1)  (G 1)  (H 1)}
合体        {(A 8)  (B 3)  ({C D} 2)  ({E F} 2)  (G 1)  (H 1)}
合体        {(A 8)  (B 3)  ({C D} 2)  ({E F} 2)  ({G H} 2)}
合体        {(A 8)  (B 3)  ({C D} 2)  ({E F G H} 4)}
合体        {(A 8)  ({B C D} 5)  ({E F G H} 4)}
合体        {(A 8)  ({B C D E F G H} 9)}
最後の合体  {({A B C D E F G H} 17)}
このアルゴリズムは常に同じ木になるとは限らない. 各回で最小重みの節が一意ではないかも知れないからだ. また二つの節が合体される順の決め方(つまりどちらが左でどちらが右か)も任意である.
Huffman木の表現
次の問題では通信文を符号化, 復号化するのにHuffman木を使い, 上に述べたアルゴリズムでHuffman木を生成するシステムを勉強しよう. まず木の表現の議論から始める.

   葉の節は記号leaf, その葉の記号と重みからなるリストで表現する:


(define (make-leaf symbol weight)
  (list 'leaf symbol weight))


(define (leaf? object)
  (eq? (car object) 'leaf))


(define (symbol-leaf x) (cadr x))


(define (weight-leaf x) (caddr x))
一般の木は, 左の枝, 右の枝, 記号の集合と重みのリストである. 記号の集合は, もっと凝った集合の表現ではなく, 単に記号のリストである. 二つの節を合体して木を作る時は, 木の重みは節の重みの和とし, 記号の集合は節の記号の集合の和集合とする. 記号の集合はリストで表現してあるから, 2.2.1節のappend手続きを使って和集合を作ることが出来る:

(define (make-code-tree left right)
  (list left
        right
        (append (symbols left) (symbols right))
        (+ (weight left) (weight right))))
木をこのように作るなら, 選択子は次の通り:

(define (left-branch tree) (car tree))


(define (right-branch tree) (cadr tree))


(define (symbols tree)
  (if (leaf? tree)
      (list (symbol-leaf tree))
      (caddr tree)))


(define (weight tree)
  (if (leaf? tree)
      (weight-leaf tree)
      (cadddr tree)))
手続きsymbolsweightは, 葉から呼び出されたか, 一般の木から呼び出されたかにより, 多少違うことをやらなければならない. これは 汎用手続き(generic procedure)(一種類以上のデータを扱うことの出来る手続き)の単純な例である. これについては2.4, 2.5節でいろいろいいたいことがある.
復号化手続き
次の手続きは復号化アルゴリズムを実装する. それらは引数として0と1のリストとHuffman木をとる.

(define (decode bits tree)
  (define (decode-1 bits current-branch)
    (if (null? bits)
        '()
        (let ((next-branch
               (choose-branch (car bits) current-branch)))
          (if (leaf? next-branch)
              (cons (symbol-leaf next-branch)
                    (decode-1 (cdr bits) tree))
              (decode-1 (cdr bits) next-branch)))))
  (decode-1 bits tree))

(define (choose-branch bit branch)
  (cond ((= bit 0) (left-branch branch))
        ((= bit 1) (right-branch branch))
        (else (error "bad bit -- CHOOSE-BRANCH" bit))))
手続きdecode-1は二つの引数: 残りのビットのリストと木の中の現在位置をとる. 木を「下降」しながら, リストの次のビットが0か1かに従って, 左か右か枝を決める. (手続きchoose-branchによる.) 葉に到達すると, 葉のところの記号を通信文の次の記号とするが, 木のルートから出発して通信文の残りを復号化した結果にこの記号をconsして返す. choose-branchの最後の節のエラーチェックに注意しよう. これは手続きが入力データに0でも1でもない, 何かを見つけた時に文句をいう.
重みつき要素の集合
われわれの木の表現で, 葉でない節はそれぞれ単純なリストで表現する記号の集合を持っている. しかし上で論じた木の生成アルゴリズムは, 二つの最小の項を次々と合体させながら, 葉と木の集合での作業を要求する. 集合中の最小の項を繰り返し探す必要があるので, この種の集合には, 順序づけられた表現を使うと便利である.

   葉と木の集合を, 重みの大きくなる順に並べたリストの要素として表現する. 次の集合を構成するadjoin-set手続きは, 問題2.61に書いたものと似ている; しかし項は重みで比べられ, 集合に追加される要素は決して前から存在していることはない.


(define (adjoin-set x set)
  (cond ((null? set) (list x))
        ((< (weight x) (weight (car set))) (cons x set))
        (else (cons (car set)
                    (adjoin-set x (cdr set))))))

   次の手続きは((A 4) (B 2) (C 1) (D 1))のような記号と頻度の対のリストをとり, Huffmanアルゴリズムにより, 直ぐに合体される葉の, 最初の順序づけられた集合を構成する.


(define (make-leaf-set pairs)
  (if (null? pairs)
      '()
      (let ((pair (car pairs)))
        (adjoin-set (make-leaf (car pair)    ; 記号
                               (cadr pair))  ; 頻度
                    (make-leaf-set (cdr pairs))))))

問題 2.67


符号化木と例題の通信文を定義する:
(define sample-tree
  (make-code-tree (make-leaf 'A 4)
                  (make-code-tree
                   (make-leaf 'B 2)
                   (make-code-tree (make-leaf 'D 1)
                                   (make-leaf 'C 1)))))

(define sample-message '(0 1 1 0 0 1 0 1 0 1 1 1 0))
通信文を復号化するのにdecode手続きを使い, 結果を示せ.

問題 2.68


encode手続きは引数として通信文と木をとり, 符号化された通信文のビットのリストを作る.

(define (encode message tree)
  (if (null? message)
      '()
      (append (encode-symbol (car message) tree)
              (encode (cdr message) tree))))
encode-symbolは自分で書く手続きで, 与えられた木に従って与えられた記号を符号化したビットのリストを返すものである. encode-symbolの設計では, 記号が木になければ, エラーとしなければならない. 出来た手続きを問題2.67で得た結果と, 例題の木を使って符号化し, 元の例題の通信文と同じかどうかを見てテストせよ.

問題 2.69


次の手続きは引数として記号と頻度の対のリストをとり, (どの記号も一つの対以外には現れない.) Huffmanアルゴリズムに従い, Huffman符号化木を生成する.
(define (generate-huffman-tree pairs)
  (successive-merge (make-leaf-set pairs)))
make-leaf-setは上にある手続きで, 対のリストを葉の順序づけられた集合へ変換する. successive-mergeは自分で書く手続きで, make-code-treeを使い, 集合の最小重みの要素を順に合体させ, 要素が一つになったら止める. それが目的のHuffman木である. (この手続きは多少ややこしいが, 複雑ではない. 複雑な手続きを設計していると思ったら, 確実にどこか違っている. 順序づけられた集合の表現を使っていることを活用しなければならない.)

問題 2.70


次の八記号のアルファベットと相対頻度は, 1950年代のロックの歌の叙情詩を効率よく符号化するよう設計された. (「アルファベット」の「記号」は必ずしも個々の文字ではないことに注意)
A     2          NA   16
BOOM  1          SHA   3
GET   2          YIP   9
JOB   2          WAH   1
(問題2.69の)generate-huffman-treeを使って対応するHuffman木を生成し, (問題2.68の)encodeを使って次の通信文を符号化せよ:

Get a job
Sha na na na na na na na na
Get a job
Sha na na na na na na na na
Wah yip yip yip yip yip yip yip yip yip
Sha boom

符号化に何ビット必要か. 八記号アルファベットの固定長符号を使うとこの歌を符号化するのに必要な最小ビット数はいくらか.

問題 2.71


n記号のアルファベットのHuffman木があるとし, 記号の相対頻度が1, 2, 4, ... ,2n-1としよう. n=5とn=10の木を描け. (一般のnの)こういう木で, 最高頻度の記号を符号化するのに必要なビット数はいくらか. 最低頻度の記号ではどうか.

問題 2.72


問題2.68で設計した符号化手続きを考える. 記号を符号化するのに必要なステップ数の増加の程度は何か. 各節で記号のリストを探索するのに必要なステップ数を忘れないように. この問題に一般的に答えるのは難しい. n個の記号の相対頻度が問題2.71にあるような特別な場合を考え, アルファベットの最高頻度と最低頻度の記号を符号化するのに必要なステップ数の(nの関数としての) 増加の程度を答えよ.



42 Huffman符号の数学的性質についての議論は Hamming 1980参照

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