39

私はハフマン エンコーディング/デコーディング ツールを作成しており、出力ファイル内に保存するために作成されたハフマン ツリーを保存する効率的な方法を探しています。

現在、私が実装している 2 つの異なるバージョンがあります。

  1. これは、ファイル全体を 1 文字ずつメモリに読み込み、ドキュメント全体の度数分布表を作成します。これは、ツリーを 1 回出力するだけでよいため、入力ファイルが小さい場合を除いて、効率はそれほど大きな問題ではありません。
  2. 私が使用しているもう 1 つの方法は、サイズが約 64 キロバイトのデータのチャンクを読み取り、それに対して周波数分析を実行し、ツリーを作成してエンコードすることです。ただし、この場合、すべてのチャンクの前に、デコーダーがツリーを再構築し、エンコードされたファイルを適切にデコードできるように、周波数ツリーを出力する必要があります。できるだけ多くのスペースを節約したいので、これが効率の良いところです。

これまでの検索では、ツリーをできるだけ小さなスペースに格納する良い方法が見つかりませんでした。StackOverflow コミュニティが良い解決策を見つけるのに役立つことを願っています!

4

6 に答える 6

89

バイト編成のストリーム/ファイルの上にビット単位のレイヤーを処理するコードを既に実装する必要があるため、ここに私の提案があります。

実際の周波数は保存しないでください。デコードには必要ありません。ただし、実際のツリーは必要です。

したがって、各ノードについて、ルートから開始します。

  1. リーフノードの場合: 1 ビット + N ビットの文字/バイトを出力
  2. リーフノードでない場合は、0 ビットを出力します。次に、両方の子ノード (左が先、次に右が先) を同じ方法でエンコードします。

読むには、次のようにします。

  1. ビットを読み取ります。1 の場合、N ビットの文字/バイトを読み取り、その周りに子を持たない新しいノードを返します
  2. ビットが 0 の場合、左右の子ノードを同じ方法でデコードし、それらの子を含むそれらの周りの新しいノードを返しますが、値は返しません。

リーフノードは基本的に、子を持たないノードです。

このアプローチを使用すると、出力を書き込む前に出力の正確なサイズを計算して、努力を正当化するのに十分な利益があるかどうかを判断できます。これは、各文字の頻度を含むキーと値のペアの辞書があることを前提としています。ここで、頻度は実際の出現回数です。

計算用の擬似コード:

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

例 (簡略化、プロパティの使用など) ノードの実装:

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

特定の例からの出力例を次に示します。

入力: AAAAAABCCCCCCDDEEEEE

周波数:

  • あ:6
  • B: 1
  • 子:6
  • D: 2
  • え:5

各文字はわずか 8 ビットなので、ツリーのサイズは 10 * 5 - 1 = 49 ビットになります。

ツリーは次のようになります。

      20
  ----------
  |        8
  |     -------
 12     |     3
-----   |   -----
A   C   E   B   D
6   6   5   1   2

したがって、各文字へのパスは次のようになります (0 が左、1 が右)。

  • あ:00
  • 乙:110
  • 子:01
  • D:111
  • え:10

したがって、出力サイズを計算するには:

  • A: 6 オカレンス * 2 ビット = 12 ビット
  • B: 1 オカレンス * 3 ビット = 3 ビット
  • C: 6 オカレンス * 2 ビット = 12 ビット
  • D: 2 オカレンス * 3 ビット = 6 ビット
  • E: 5 回 * 2 ビット = 10 ビット

エンコードされたバイトの合計は 12+3+12+6+10 = 43 ビット

これをツリーの 49 ビットに追加すると、出力は 92 ビットまたは 12 バイトになります。これを、エンコードされていない元の 20 文字を格納するために必要な 20 * 8 バイトと比較すると、8 バイト節約できます。

最初のツリーを含む最終的な出力は次のとおりです。ストリーム (AE) の各文字は 8 ビットとしてエンコードされますが、0 と 1 は 1 ビットにすぎません。ストリーム内のスペースは、ツリーをエンコードされたデータから分離するためのものであり、最終出力でスペースを占有しません。

001A1C01E01B1D 0000000000001100101010101011111111010101010

コメントにある具体的な例 AABCDEF については、次のようになります。

入力: AABCDEF

周波数:

  • あ:2
  • B: 1
  • 子:1
  • D: 1
  • え:1
  • 女:1

木:

        7
  -------------
  |           4
  |       ---------
  3       2       2
-----   -----   -----
A   B   C   D   E   F
2   1   1   1   1   1

パス:

  • あ:00
  • 乙:01
  • 子:100
  • D:101
  • え:110
  • 女:111

ツリー: 001A1B001C1D01E1F = 59 ビット
データ: 000001100101110111 = 18 ビット
合計: 59 + 18 = 77 ビット = 10 バイト

元は 8 ビットの 7 文字 = 56 だったので、このような小さなデータのオーバーヘッドが大きすぎます。

于 2009-04-17T09:38:28.713 に答える
12

ツリーの生成を十分に制御できる場合は、正規のツリーを実行させることができます(たとえば、DEFLATEと同じ方法)。これは、基本的に、ツリーを構築するときにあいまいな状況を解決するためのルールを作成することを意味します。次に、DEFLATEと同様に、実際に保存する必要があるのは、各文字のコードの長さだけです。

つまり、上記のツリー/コードLasseがある場合:

  • A:00
  • B:110
  • C:01
  • D:111
  • E:10

次に、それらを2、3、2、3、2として保存できます。

そして、これは、常に同じ文字セット(ASCIIなど)を使用していると仮定すると、ハフマンテーブルを再生成するのに実際には十分な情報です。(つまり、文字をスキップすることはできません。ゼロであっても、各文字のコード長をリストする必要があります。)

ビット長(たとえば、7ビット)にも制限を設ける場合は、短い2進文字列を使用してこれらの各数値を格納できます。したがって、2,3,2,3,2は010 011 010011010になります-これは2バイトに収まります。

本当に夢中になりたい場合は、DEFLATEが行うことを実行し、これらのコードの長さの別のハフマンテーブルを作成し、そのコードの長さを事前に保存することができます。特に、「ゼロをN回続けて挿入する」ためのコードを追加して、さらに短縮するためです。

ハフマンコーディングに既に精通している場合、DEFLATEのRFCはそれほど悪くありません: http ://www.ietf.org/rfc/rfc1951.txt

于 2009-06-02T22:04:28.320 に答える
7

枝は 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」にして、前のチャンクのツリーを再利用することをテストできます。 .

于 2009-04-17T09:32:05.750 に答える
2

ツリーは通常、バイトの頻度表から作成されます。したがって、そのテーブル、または頻度でソートされたバイト自体を保存し、その場でツリーを再作成します。もちろん、これは、大きなブロックではなく、単一バイトを表すツリーを構築していることを前提としています。

更新: コメントで j_random_hacker が指摘したように、実際にはこれを行うことはできません。頻度の値自体が必要です。ツリーを構築すると、それらが組み合わされて上方に「泡立ち」ます。このページでは、度数分布表からツリーを作成する方法について説明します。おまけとして、ツリーを保存する方法を言及することで、この回答が削除されるのを防ぎます。

ハフマン ツリー自体を出力する最も簡単な方法は、ルートから始めて、最初に左側をダンプし、次に右側をダンプすることです。ノードごとに 0 を出力し、リーフごとに 1 を出力し、その後に値を表す N ビットを出力します。

于 2009-04-17T09:29:25.240 に答える