bzip2を読む ハフマン符号3
こんにちは、junkawaです。
本記事では、カノニカルハフマン符号(Canonical Huffman Code)によって、符号長が決まったシンボルに、符号を割り当てる方法を紹介します。
当該関数は、BZ2_hbAssignCodes() @ huffman.c です。
カノニカルハフマンについて
通常のハフマン符号では、ハフマン木が完成して符号長が決まっても、木の各枝に0、1の符号を割り当てる順番は自由なため、符号長から符号は一意に決まりません。
bzip2では、カノニカルハフマンという、ハフマン符号に制約を設けた符号化を用いるため、符号長から符号が一意に定まります。
一般的なハフマン符号では、符号の割当(枝への0,1の割当)は自由なのですが、カノニカルハフマンでは、「符号長が同じシンボルは、符号の辞書式順とシンボルの順序が同じ」になるように符号を割り当てる、という制約があります。
参考
https://en.wikipedia.org/wiki/Bzip2
https://en.wikipedia.org/wiki/Canonical_Huffman_code
下記、岡野原さんの解説ではカノニカルハフマンの制約は「各文字の符号の辞書式順序は、文字の出現確率の大きい順序と一致する」とのことで、bzip2の(Wikipediaの)カノニカルハフマンの定義とは若干違うようです。カノニカルハフマンにもいくつか方法があるのでしょうか。
https://codezine.jp/article/detail/376
符号の辞書式順とは、100 → 101 → 110 のように、0、1の符号の組み合わせの順序です。0が1より小さくなります。
シンボルの順序とは、0〜257のシンボルの数値順です。
符号の割り当て方
図では、シンボルの出現頻度から生成したハフマン木を表しています。
葉ノード(0〜8)がシンボルに対応します。
葉ノードより上はノード(9〜16)となります。
ノード(9〜16)の出現頻度は、子ノードの出現頻度を合わせたものになります。
例えば、ノード9の出現頻度は、葉ノード6と葉ノード3の出現頻度22と21を足した43となります。また、ノード12の出現頻度は葉ノード4とノード9の出現頻度40と43を合わせた83となります。
このハフマン木は、前回紹介した BZ2_hbMakeCodeLengths()で生成します。
図では、ハフマン木から符号を割り当てています。
ハフマン木の葉ノードがシンボルに対応しています。
また、根ノードから葉ノードまでの高さが符号長となります。
符号長2のシンボルは4のみです。
符号長3のシンボルは6、3、7、5 の4つです。
符号長4のシンボルは1、8、2、0 の4つです。
カノニカルハフマンの「符号長が同じシンボルは、符号の辞書式順とシンボルの順序が同じ」になるように符号を割り当てるために、符号長3のシンボルの符号割当順序を3、5、6、7とします(シンボルの順序)。また、符号長4のシンボルの符号割当順序を0、1、2、8とします。
符号の割当は、下記のとおりです
始めに符号長が最も短い文字(出現確率が高い文字)の符号を「0..0」とします。それ以外の文字の符号は、前の文字の符号に1を加え、文字符号長が長くなる場合は長くなった分0を最後につけます。
https://codezine.jp/article/detail/376?p=2
図の例では、符号長2のシンボル4に、「00」を割り当てます。
次に符号長3のシンボル3に、「010」を割り当てます。これは、前の符号「00」に1を加え(「01」)、符号長が2から3に長くなったので、最後に0をつけたものです。
シンボル4に「011」を割り当てます。前の符号「010」と符号長が変わらないので、最後に0はつけません。同様にシンボル5に「011」を割り当てます。
このようにして、全てのシンボルに符号を割り当てます。
コードの紹介
BZ2_hbAssignCodes() @ huffman.c
https://github.com/junkawa/bzip2/blob/master/bzip2-1.0.6/huffman.c#L152
https://github.com/junkawa/bzip2/blob/master/bzip2-1.0.6/huffman.c#L151
〜
https://github.com/junkawa/bzip2/blob/master/bzip2-1.0.6/huffman.c#L166
151: /*---------------------------------------------------*/
152: void BZ2_hbAssignCodes ( Int32 *code,
153: UChar *length,
154: Int32 minLen,
155: Int32 maxLen,
156: Int32 alphaSize )
157: {
code [出力] シンボルの符号
length [入力] シンボルの符号長
minLen [入力] 最小符号長
maxLen [入力] 最大符号長
aplhaSize [入力] シンボル数
シンボルの符号長をもとに、シンボルの符号を割り当てます。
158: Int32 n, vec, i;
159:
160: vec = 0;
vec には符号が入ります。値の意味がビット単位の符号なのでvec (vector)と呼んでいるのだと思われます。
161: for (n = minLen; n <= maxLen; n++) {
162: for (i = 0; i < alphaSize; i++)
163: if (length[i] == n) { code[i] = vec; vec++; };
164: vec <<= 1;
165: }
minLen、maxLenは無駄なループを回さないために用います。
minLen、maxLenはBZ2_hbAssignCodes() 呼び出し前に求めています(compress.c#484〜487)。
minLen、maxLenがない場合、0<=n<=17 でループを回さなければならず、1回のループで内側のループをalphaSize回まわさなければならないため非効率です。
始めに符号長が最も短い文字(出現確率が高い文字)の符号を「0..0」とします。それ以外の文字の符号は、前の文字の符号に1を加え、文字符号長が長くなる場合は長くなった分0を最後につけます。
https://codezine.jp/article/detail/376?p=2
162〜164行では、上記を実現しています。
vecの初期値が0なので、length[minLen]となる最初のシンボルは0..0 (minLenの長さ分0が続く)です。次に、vec++により、前の符号に1を加えています。符号長が長くなる場合は、164行目のvec <<= 1 により、符号の最後に0をつけます(左シフト)。
166: }
まとめ
カノニカルハフマン符号による、シンボルへの符号割当を紹介しました。
次回は、今回紹介した関数を使って、MTF後のデータにハフマン符号を適用する部分を紹介します。
ご覧下さりありがとうございます。いただいたサポートは図書館への交通費などに使わせていただきます。