ハフマン符号

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

ハフマン符号 (Huffman coding)とは、1952年デビット・ハフマン英語版によって開発された符号である。 コンパクト符号エントロピー符号の一つ。JPEGZIP (Deflate) などの圧縮フォーマットで使用されている。

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

目次

種類 [編集]

符号化の原理上、木を構成する前に各記号の出現頻度をあらかじめ知っておく必要がある。1度目の読み込みで、データのすべての記号を調べておき、2度目に符号化を行う方法を、静的ハフマン符号 (Static Huffman coding) と呼ぶ。一方、記号を1つ読み込むごとに木を作り直し、1度の読み込みで符号化を行う方法を適応型ハフマン符号 (動的ハフマン符号, Adaptive Huffman coding) と呼ぶ。

なお、Deflateではダイナミックハフマン符号を使っているが、これは静的ハフマン符号の一種であり、適応型ハフマン符号の事ではない。同じく Deflate では固定ハフマン符号も使っているが、これは、記号の出現頻度に関係なく符号化する物である。

符号化の原理 [編集]

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

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

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

[編集]

ハフマン木

入力 DAEBCBACBBBC に対して上記のアルゴリズムを適用すると、

出現頻度と割り当てられた符号

文字 個数 符号
B 5 0
C 3 10
A 2 110
D 1 1110
E 1 1111

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

実装 [編集]

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

関連項目 [編集]