ハフマン符号化——LispとPython実現

16655 ワード

掘金のオリジナル機能をサポートします~
原文住所:ハフマン符号化——LispとPython実現
SICP第2章では,データの抽象化を主唱し,Lisp言語のデータ構造として簡単に理解できる.もちろん、このような理解は、Lispにデータとプロセスの違いが存在しないため、このようなデータ抽象的なプロセスを記述するための追加の概念を全く必要としないため、遷移としてのみ使用することができる.2.3.4ハフマン符号化を例として、どのようにLispの中でハフマンツリーデータ構造の表現と操作を実現するかを示し、本文はこの小節の練習問題(完全なハフマン符号化ツリーの生成、復号と符号化)を完成した上で、Lisp(ここではDrRacketの#lang sicpモード、すなわちSchemeに近似する)とPythonバージョンのプログラムを比較する.
1.ハフマンの木
ハフマン符号化は重み付け二叉木に基づき、Pythonでハフマン木を表す.
class Node(object):
  def __init__(self, symbol='', weight=0):
    self.left = None
    self.right = None
    self.symbol = symbol #   
    self.weight = weight #   

Lispはリスト(List)で表される:
(define (make-leaf symbol weight) (list 'leaf symbol weight))
(define (make-tree left right)
  (list left
        right
        (append (symbols left) (symbols right))  ;             
        (+ (weight left) (weight right)))
)

;; methods
(define (leaf? object) (eq? (car object) 'leaf))
(define (symbol-leaf leaf) (cadr leaf))
(define (weight-leaf leaf) (caddr leaf))

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

2.ハフマンアルゴリズム
最適二叉木を生成するために,ハフマン符号化は文字出現頻度を重みとし,現在重みが最も小さい2つのノードを二叉木を生成する左右の分岐として毎回選択し,重みの和をルートノードの重みとし,この貪欲なアルゴリズムに従って,重み付き経路長が最も短い二叉木を底から上へ生成する.
このアルゴリズムによれば、「文字-周波数」の統計情報からハフマンツリーを構築することができ、Python実装では、すべてのノードを重みで並べ替えるたびに、
# frequency = {'a': 1, 'b': 3, 'c': 2}
def huffman_tree(frequency):
  SIZE = len(frequency)
  nodes = [Node(char, frequency.get(char)) for char in frequency.keys()]
  for _ in range(SIZE - 1):
    nodes.sort(key=lambda n: n.weight) #              
    left = nodes.pop(0)
    right = nodes.pop(0)
    parent = Node('', left.weight + right.weight)
    parent.left = left
    parent.right = right
    nodes.append(parent)
  return nodes.pop()

Lispは通常、再帰的プロセスを用いてループ動作を完了し、挿入ソートのような秩序化された集合を以下のように実現する.
(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))))))

次のPythonリストの推定機能に似たプロセスは、Pythonの簡潔さと美しさをより感じることができるかもしれません.
(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))))))

最後に、「文字-周波数」の秩序化されたセットに基づいてハフマン符号化ツリーを生成します.
(define (generate-huffman-tree pairs)
  (define (successive-merge leaf-set)
    (cond ((null? leaf-set) '())
          ((null? (cdr leaf-set)) (car leaf-set))
          (else (successive-merge (adjoin-set 
            (make-code-tree (car leaf-set)
                            (cadr leaf-set))
            (cddr leaf-set))))
    ))
  (successive-merge (make-leaf-set pairs)))

3.符号化と復号化
ハフマンツリーがあれば、符号化と復号が可能です.符号化プロセスは、文字が存在するリーフノードと、ルートノードからリーフノードへの経路を見つける必要があり、左サブツリー検索で"0"、右サブツリーで"1"と表記されるたびに、二叉木のシーケンスを用いて、リーフノードと経路を再帰的に見つけることができる.
def encode(symbol, tree):
  bits = None

  def preOrder(tree, path):
    if tree.left:
      preOrder(tree.left, path + "0")
    if tree.right:
      preOrder(tree.right, path + "1")
    if tree.isLeaf() and tree.symbol == symbol:
      nonlocal bits
      bits = path

  preOrder(tree, "")
  return bits

Lispのツリー内の各レイヤーのルートノードは、すべてのサブツリーに表示される文字をキャッシュし、時間を空間的にスワップします.
(define (left-branch tree) (car tree))
(define (right-branch tree) (cadr tree))

(define (encode-symbol char tree)
  (cond ((null? char) '())
        ((leaf? tree) '())
        ((memq char (symbols (left-branch tree))) 
          (cons 0 
            (encode-symbol char (left-branch tree))))
        ((memq char (symbols (right-branch tree)))
          (cons 1
            (encode-symbol char (right-branch tree))))
        (else (display "Error encoding char "))))

復号プロセスは、"0"に遭遇して左のサブツリーを取り、"1"に遭遇して右のサブツリーを取るルールに従えば、より直接的である.
def decode(bits, tree):
  result = ""
  root = tree
  for bit in bits:
    if bit == "0":
      node = root.left
    elif bit == "1":
      node = root.right
    else:
      return "Code error: {}".format(bit)
  if node.isLeaf():
    result += node.symbol
      root = tree
  else:
    root = node
  return result
(define (choose-branch bit branch)
  (cond ((= bit 0) (left-branch branch))
        ((= bit 1) (right-branch branch))
        (else (display "bat bit: CHOOSE-BRANCH" bit))))

(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))

上記のアルゴリズムは、重み値が等しく、左右のサブツリー割り当ての問題が発生する可能性があるため、生成されるたびにハフマンツリーが一意であることを保証することはできないが、同じツリーに対して符号化および復号化の結果は相互に通じる.
4.まとめ
開発言語としてLispが使われるとは限らないが、優れた思想を学び、吸収することを妨げるものではない.SICPの前の2章はそれぞれプロセスに対する抽象とデータに対する抽象を紹介し、その中の1つの重要な思想は符号化の本質が計算プロセスに対する記述であり、このような記述はある特定の文法やデータ構造にこだわらない.プロセスに対する抽象(例えば(define (add a b) (+ a b)))とデータに対する抽象(例えば(define (make-leaf symbol weight) (list 'leaf symbol weight)))との間に本質的な相違はない.
Pythonはプロフィール、優雅さを確保すると同時に、驚くべき柔軟性を持っており、Schemeの文法を真似て上記のすべてのプログラムを完成することができます.
def make-leaf(symbol, weight):
  return ['leaf', symbol, weight]
def leaf?(obj):
  return obj[0] == 'leaf'

以上のようにすることは意味がないが,プロセスの記述を関数レベルに抽象化し,関数を操作する思想はPythonにおいても同様に重要である.
上のコードの大部分は旅先の列車や自動車で完成しているので、オフラインプログラミングの「楽しみ」を体験する機会は少ないですが、sortnonlocalの使い方はPyTipsを書いたときのまとめのおかげなので、0xFFをいっぱい書く時間があることを望んでいます.