ハフマン符号

出典: フリー百科事典『ウィキペディア(Wikipedia)』
移動先: 案内検索

テンプレート:複数の問題 ハフマン符号 (Huffman coding)とは、1952年テンプレート:仮リンクによって開発された符号である。 コンパクト符号エントロピー符号の一つ。JPEGZIP (Deflate) などの圧縮フォーマットで使用されている。

シャノン符号化が最適ではない場合が存在する不完全な符号であったのに対し、ハフマン符号は(整数の符号語長という制約のもとでは、)常に最適な符号を構成できる。擬似的に実数の符号語長を割り振る算術符号と比較すれば、データ圧縮効率は劣る。ただし、算術符号やその他の高効率の符号化法と異なり、特許の問題が無い。

種類

符号化の原理上、木を構成する前に各記号の出現頻度をあらかじめ知っておく必要がある。

ファイルなど固定長のデータに対し、1度全部読み込んで、データのすべての記号を調べて符号木を構築しておき、もう1度頭から読み直して符号化を行う方法が、静的ハフマン符号 (Static Huffman coding) である。

deflateでは、データの全部ではなく、ブロック毎に符号を作っている。これをdeflateではダイナミックハフマン符号(英: Dynamic Huffman coding)と呼んでいる。

一方、最初の状態では頻度情報を持たず、記号を1個読み込むごとに符号木を作り直す方法は適応型ハフマン符号 (Adaptive Huffman coding) である。これを指して日本では動的ハフマン符号と呼ぶことが多い。

符号化の原理

データに出現する記号の個数を求める。 それが木構造の葉に相当すると見なし、ボトムアップで木を構成する。

まず、葉を含むすべての節点のうち、親を持たないものを集める。 その中から、最小の値をもつものと2番目に小さい値をもつものを取り出す。 それらを子供にもつ新しい節点を作る。 このとき、新しい節点の値は、両方の子供の値の和とする。

以上を繰り返して根節点まで到達して木が完成される。 次に、根から順に左右に0と1の値を割り振っていく(左右のどちらに0と1を与えるかは任意)。 すると、それぞれの葉(記号)に対して、一意にビット列が与えられる。 この記号とビット列の関係をもとに、もとのデータの記号をビット列に変換していくことで符号化が行われる。

入力 "this is an example of a huffman tree" に対して上記のアルゴリズムを適用すると、

文字 個数 符号
空白 7 111
a 4 010
e 4 000
f 3 1101
h 2 1010
i 2 1000
m 2 0111
n 2 0010
s 2 1011
t 2 0110
l 1 11001
o 1 00110
p 1 10011
r 1 11000
u 1 00111
x 1 10010

が得られ、個数の多い文字ほど短い符号が割り当てられていることがわかる。

実装

Groovyで実装を説明する。まず、符号情報とハフマン木のクラスを作る。

class CodeInfo { int code, codeSize }

class TreeNode implements Comparable<TreeNode> {
  byte value; int occurrence; List<TreeNode> children;
  int compareTo(TreeNode that) { this.occurrence - that.occurrence }
}

すると、バイト値→符号情報のテーブルは以下のようにして作れる。

CodeInfo[] createValue2Code(byte[] data) {
  // 各バイト値の発生回数を数える
  TreeNode[] nodes = new TreeNode[256]
  for (int i in 0..<256) { nodes[i] = new TreeNode(value: i) }
  for (byte v in data) { nodes[v].occurrence++ }

  // ハフマン木を作成
  PriorityQueue<TreeNode> queue = new PriorityQueue<TreeNode>(nodes as List)
  while (queue.size() > 1) {
    TreeNode n1 = queue.poll(), n2 = queue.poll()
    queue << new TreeNode(occurrence: n1.occurrence + n2.occurrence, children: [n1, n2])
  }
  TreeNode root = queue.poll()

  // 深さ優先探索でバイト値→符号情報を作成
  CodeInfo[] value2code = new CodeInfo[256]
  dfs(value2code, root, 0, 0)
  return value2code
}

void dfs(CodeInfo[] value2code, TreeNode node, int code, int codeSize) {
  if (node.children == null) {
    value2code[node.value] = new CodeInfo(code: code, codeSize: codeSize)
  } else {
    dfs(value2code, node.children[0], code << 1, codeSize + 1)
    dfs(value2code, node.children[1], (code << 1) + 1, codeSize + 1)
  }
}

圧縮のために以下のようなビットストリームのクラスを作る。

class BitStream {
  BitSet bs = new BitSet(); int len = 0, pos = 0;
  void write(int v, int bits) {
    for (int i in 0..<bits) { bs[len++] = ((v >>> i) & 1) != 0 }
  }
  int read(int bits) {
    int v = 0; for (int i in 0..<bits) { if (bs[pos++]) { v |= 1 << i } }
    return v
  }
  String toString() { "length = $len, {" + (0..<len).findAll({ bs[it] }).join(", ") + "}" }
}

すると、バイト値→符号情報のテーブルを使い以下のようにして圧縮できる。

void compress(BitStream bs, byte[] data, CodeInfo[] value2code) {
  for (byte v in data) {
    CodeInfo codeInfo = value2code[v]
    bs.write(codeInfo.code, codeInfo.codeSize)
  }
}

関連項目

テンプレート:データ圧縮テンプレート:Asbox