私はハフマンエンコーディング/デコーディングツールを作成していて、出力ファイル内に格納するために作成されたハフマンツリーを格納する効率的な方法を探しています。
現在、私が実装している2つの異なるバージョンがあります。
これまでの検索では、ツリーをできるだけ小さなスペースに格納するための適切な方法が見つかりませんでした。StackOverflowコミュニティが良い解決策を見つけるのに役立つことを願っています!
バイト構成のストリーム/ファイルの上にビット単位のレイヤーを処理するコードをすでに実装している必要があるので、これが私の提案です。
実際の周波数は保存しないでください。デコードには必要ありません。ただし、実際のツリーが必要です。
したがって、各ノードについて、ルートから開始します。
読むには、次のようにします。
リーフノードは、基本的には子を持たないノードです。
このアプローチを使用すると、出力を書き込む前に正確なサイズを計算して、作業が正当化されるのに十分かどうかを判断できます。これは、各文字の頻度を含むキー/値ペアのディクショナリがあることを前提としています。頻度は実際の出現回数です。
計算用の擬似コード:
Tree-size = 10 * NUMBER_OF_CHARACTERS - 1
Encoded-size = Sum(for each char,freq in table: freq * len(PATH(char)))
ツリーサイズの計算では、リーフノードと非リーフノードが考慮され、インラインノードは文字数より1つ少なくなります。
SIZE_OF_ONE_CHARACTERはビット数であり、これらの2つは、ツリーに対する+エンコードされたデータが占める合計ビット数を提供します。
PATH(c)は、ルートからツリー内のその文字までのビットパスを生成する関数/テーブルです。
これを行うためのC#のような擬似コードを次に示します。これは、1文字が単なるバイトであると想定しています。
void EncodeNode(Node node, BitWriter writer)
{
if (node.IsLeafNode)
{
writer.WriteBit(1);
writer.WriteByte(node.Value);
}
else
{
writer.WriteBit(0);
EncodeNode(node.LeftChild, writer);
EncodeNode(node.Right, writer);
}
}
それを読み戻すには:
Node ReadNode(BitReader reader)
{
if (reader.ReadBit() == 1)
{
return new Node(reader.ReadByte(), null, null);
}
else
{
Node leftChild = ReadNode(reader);
Node rightChild = ReadNode(reader);
return new Node(0, leftChild, rightChild);
}
}
例(簡略化、プロパティの使用など)Node実装:
public class Node
{
public Byte Value;
public Node LeftChild;
public Node RightChild;
public Node(Byte value, Node leftChild, Node rightChild)
{
Value = value;
LeftChild = leftChild;
RightChild = rightChild;
}
public Boolean IsLeafNode
{
get
{
return LeftChild == null;
}
}
}
特定の例からの出力例を以下に示します。
入力:AAAAAABCCCCCCDCDEEEEEE
頻度:
各文字はわずか8ビットであるため、ツリーのサイズは10 * 5-1 = 49ビットになります。
ツリーは次のようになります。
20
----------
| 8
| -------
12 | 3
----- | -----
A C E B D
6 6 5 1 2
したがって、各文字へのパスは次のようになります(0は左、1は右です)。
したがって、出力サイズを計算するには:
エンコードされたバイトの合計は12 + 3 + 12 + 6 + 10 = 43ビット
それをツリーの49ビットに追加すると、出力は92ビット、つまり12バイトになります。エンコードされていない元の20文字を格納するために必要な20 * 8バイトと比較すると、8バイト節約できます。
最初のツリーを含む最終的な出力は次のとおりです。ストリーム内の各文字(A〜E)は8ビットとしてエンコードされますが、0と1は単一ビットです。ストリーム内のスペースは、エンコードされたデータからツリーを分離するためだけのものであり、最終的な出力ではスペースを占有しません。
001A1C01E01B1D 0000000000001100101010101011111111010101010
コメントAABCDEFにある具体的な例では、次のようになります。
入力:AABCDEF
頻度:
木:
7
-------------
| 4
| ---------
3 2 2
----- ----- -----
A B C D E F
2 1 1 1 1 1
パス:
ツリー:001A1B001C1D01E1F = 59ビット
データ:000001100101110111 = 18ビット
合計:59 + 18 = 77ビット= 10バイト
オリジナルは8ビット= 56の7文字だったため、そのような小さなデータのオーバーヘッドが多すぎます。
ツリーの生成を十分に制御できる場合は、標準的なツリーを実行できます(たとえば、 [〜#〜] deflate [〜#〜] と同じ方法)。これは、基本的には次のことを意味しますツリーを構築するときにあいまいな状況を解決するルールを作成します。次に、DEFLATEと同様に、実際に保存する必要があるのは、各文字のコードの長さだけです。
つまり、上記のツリー/コードLasseがある場合:
次に、2、3、2、3、2として保存できます。
そして、常に同じ文字セット(ASCIIなど)を使用していると仮定すると、ハフマンテーブルを再生成するのに十分な情報です。 (これは、文字をスキップできなかったことを意味します。たとえゼロであっても、文字ごとにコード長をリストする必要があります。)
ビット長(たとえば7ビット)にも制限を設ける場合、短いバイナリ文字列を使用してこれらの各数値を格納できます。したがって、2、3、2、3、2は010 011 010 011 010-2バイトに収まります。
reallyクレイジーにしたい場合は、DEFLATEが行うことを実行し、これらのコードの長さの別のハフマンテーブルを作成し、そのコード長を格納できます。予め。特に、「Nをゼロ回続けて挿入する」ためのコードを追加して、物事をさらに短縮しています。
ハフマンコーディングに既に慣れている場合は、DEFLATEのRFCはそれほど悪くありません。 http://www.ietf.org/rfc/rfc1951.txt
枝は0、葉は1です。最初に木の深さを移動して、その「形状」を取得します。
e.g. the shape for this tree
0 - 0 - 1 (A)
| \- 1 (E)
\
0 - 1 (C)
\- 0 - 1 (B)
\- 1 (D)
would be 001101011
同じ深さの最初のAECBDにある文字のビットを続けます(読んでいると、ツリーの形状から予想される文字数がわかります)。次に、メッセージのコードを出力します。次に、出力用の文字に分割できる長いビット列があります。
チャンクしている場合、次のチャックのためにツリーを保存することは、前のチャンクのためにツリーを再利用するのと同じくらい効率的で、前のチャンクからツリーを再利用するためのインジケーターとしてツリー形状が「1」であることをテストできます。 。
ツリーは通常、バイトの頻度表から作成されます。そのため、そのテーブルを格納するか、バイト自体を頻度でソートして、その場でツリーを再作成します。もちろん、これは、より大きなブロックではなくシングルバイトを表すためにツリーを構築していることを前提としています。
[〜#〜] update [〜#〜]:コメントでj_random_hackerによって指摘されているように、実際にはこれを行うことはできません。頻度値自体。それらを組み合わせて、ツリーを構築するときに上向きに「バブル」します。 このページ は、頻度表からツリーを構築する方法を説明しています。おまけとして、ツリーを保存する方法について言及することで、この回答が削除されるのを防ぐこともできます。
ハフマンツリー自体を出力する最も簡単な方法は、ルートから始めて、最初に左側、次に右側をダンプすることです。ノードごとに0を出力し、リーフごとに1を出力し、その後に値を表すNビットを続けます。
より良いアプローチ
木:
7
-------------
| 4
| ---------
3 2 2
----- ----- -----
A B C D E F
2 1 1 1 1 1 : frequencies
2 2 3 3 3 3 : tree depth (encoding bits)
次に、このテーブルを導出します。
depth number of codes
----- ---------------
2 2 [A B]
3 4 [C D E F]
同じバイナリツリーを使用する必要はありません。計算されたツリーの深さ、つまりエンコードビット数を保持するだけです。したがって、ツリーの深さ順に並べられた非圧縮値のベクトル[A B C D E F]を保持し、この個別のベクトルではなく相対インデックスを使用します。次に、各深度の整列されたビットパターンを再作成します。
depth number of codes
----- ---------------
2 2 [00x 01x]
3 4 [100 101 110 111]
すぐにわかるのは、各行の最初のビットパターンだけが重要であることです。次のルックアップテーブルを取得します。
first pattern depth first index
------------- ----- -----------
000 2 0
100 3 2
このLUTのサイズは非常に小さく(ハフマンコードが32ビット長であっても、32行しか含まれていません)、実際、最初のパターンは常にnullです。パターンのバイナリ検索を実行するときは、完全に無視できます。 (ここでは、ビット深度が2または3であるかどうかを確認し、関連するデータがベクトルに格納されている最初のインデックスを取得するために、1つのパターンのみを比較する必要があります)この例では、最大31の値の検索スペースで入力パターンの高速バイナリ検索を実行する必要があります。つまり、最大5つの整数比較です。これらの31の比較ルーチンを31のコードで最適化して、すべてのループを回避し、整数バイナリルックアップツリーを参照するときに状態を管理する必要があります。このすべてのテーブルは小さな固定長に適合します(32ビット以下のハフマンコードの場合、LUTは最大31行を必要とし、上記の2つの列は最大32行を埋めます)。
言い換えると、上記のLUTには、ビット深度値を格納するために、それぞれ32ビットサイズの31 int、32バイトが必要です。ただし、深度列(および深度1の最初の行)を暗黙指定することで、これを回避できます。
first pattern (depth) first index
------------- ------- -----------
(000) (1) (0)
000 (2) 0
100 (3) 2
000 (4) 6
000 (5) 6
... ... ...
000 (32) 6
したがって、LUTには[000、100、000(30times)]が含まれています。その中で検索するには、入力ビットパターンが2つのパターンの間にある位置を見つける必要があります。これは、このLUTの次の位置のパターンより低く、現在の位置のパターン以上である必要があります(両方の位置の場合)同じパターンが含まれている場合、現在の行は一致せず、入力パターンは以下に適合します)。次に、分割して征服し、最大5つのテストを使用します(バイナリ検索には、if/then/elseのネストされたレベルが5つ埋め込まれた単一のコードが必要です、32のブランチがあり、到達したブランチは、直接にはないビット深度を示します保存する必要があります。次に、最初のインデックスを返すために、2番目のテーブルに対して直接インデックス付けされた単一のルックアップを実行します。デコードされた値のベクトルの最終インデックスを追加的に導出します)。
ルックアップテーブルでの位置を取得すると(1列目で検索)、すぐに入力から取得するビット数と、ベクトルの開始インデックスを取得します。最初のインデックスを差し引いた後の基本的なビットマスキングにより、取得したビット深度を使用して、調整されたインデックス位置を直接導出できます。
要約すると、リンクされたバイナリツリーを決して格納せず、ルックアップを実行するためにループを必要としません。31パターンのテーブルの固定位置でパターンを比較する5つのネストされたifsと、開始オフセットを含む31 intのテーブルを必要としますデコードされた値のベクトル(ネストされたif/then/elseテストの最初のブランチでは、ベクトルへの開始オフセットが暗示され、常にゼロです。これは、最短コードと一致するために取られる最も頻繁なブランチでもあります。これは最も頻繁にデコードされた値用です)。