web-dev-qa-db-fra.com

Manière efficace de stocker l'arbre de Huffman

J'écris un outil d'encodage/décodage Huffman et je cherche un moyen efficace de stocker l'arbre Huffman créé pour être stocké à l'intérieur du fichier de sortie.

Actuellement, je suis en train de mettre en œuvre deux versions différentes.

  1. Celui-ci lit le fichier entier en mémoire caractère par caractère et construit un tableau de fréquences pour tout le document. Cela ne nécessiterait qu'une sortie de l'arborescence une seule fois. L'efficacité n'est donc pas un gros problème, sauf si le fichier d'entrée est petit.
  2. L’autre méthode que j’utilise consiste à lire un bloc de données d’une taille d’environ 64 kilo-octets et à exécuter l’analyse de fréquence par dessus, à créer un arbre et à le coder. Cependant, dans ce cas, avant chaque morceau, je devrai sortir mon arbre de fréquences afin que le décodeur puisse reconstruire son arbre et décoder correctement le fichier encodé. C’est là que l’efficacité est au rendez-vous car je souhaite économiser le plus d’espace possible.

Jusqu'ici, dans mes recherches, je n'ai pas trouvé le meilleur moyen de stocker l'arbre dans le moins de place possible. J'espère que la communauté StackOverflow pourra m'aider à trouver une bonne solution!

29
X-Istence

Puisque vous devez déjà implémenter du code pour gérer une couche au-dessus de votre flux/fichier organisé par octets, voici ma proposition.

Ne stockez pas les fréquences réelles, elles ne sont pas nécessaires au décodage. Cependant, vous avez besoin de l'arbre réel.

Donc pour chaque noeud, en partant de la racine:

  1. Si noeud-feuille: sortie 1 bit + caractère/octet
  2. Sinon nœud feuille, sortie 0 bit. Encodez ensuite les deux nœuds enfants (à gauche puis à droite) de la même manière

Pour lire, faites ceci:

  1. Lire un peu. Si 1, lit le caractère/octet N-bit, retourne le nouveau noeud autour de lui sans enfants
  2. Si bit vaut 0, décode les nœuds enfants gauche et droit de la même manière et renvoie le nouveau nœud autour d'eux avec ces enfants, mais aucune valeur

Un nœud feuille est essentiellement un nœud qui n'a pas d'enfants.

Avec cette approche, vous pouvez calculer la taille exacte de votre sortie avant de l'écrire, pour déterminer si les gains suffisent à justifier l'effort. Cela suppose que vous disposiez d'un dictionnaire de paires clé/valeur contenant la fréquence de chaque caractère, où fréquence correspond au nombre réel d'occurrences.

Pseudo-code pour le calcul:

Tree-size = 10 * NUMBER_OF_CHARACTERS - 1
Encoded-size = Sum(for each char,freq in table: freq * len(PATH(char)))

Le calcul de la taille de l'arborescence prend en compte les nœuds feuilles et non-feuilles, et il existe un nœud en ligne de moins qu'il n'y a de caractères.

SIZE_OF_ONE_CHARACTER serait le nombre de bits, et ces deux vous donnerait le nombre total de bits que mon approche occupera pour l’arbre + les données codées.

PATH (c) est une fonction/table qui donnerait le chemin de bits de la racine à ce caractère dans l’arbre.

Voici un pseudo-code à la recherche de C #, qui suppose qu'un caractère n'est qu'un simple octet.

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

Pour le relire dans:

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

Un exemple (simplifié, propriétés d'utilisation, etc.) Implémentation de nœud:

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

Voici un exemple de sortie d'un exemple spécifique.

Entrée: AAAAAABCCCCCCDDEEEEE

Fréquences:

  • A: 6
  • B: 1
  • C: 6
  • D: 2
  • E: 5

Chaque caractère ne fait que 8 bits, la taille de l’arbre sera donc de 10 * 5 - 1 = 49 bits.

L'arbre pourrait ressembler à ceci:

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

Ainsi, les chemins d'accès à chaque caractère sont les suivants (0 est à gauche, 1 est à droite):

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

Donc, pour calculer la taille de sortie:

  • A: 6 occurrences * 2 bits = 12 bits
  • B: 1 occurrence * 3 bits = 3 bits
  • C: 6 occurrences * 2 bits = 12 bits
  • D: 2 occurrences * 3 bits = 6 bits
  • E: 5 occurrences * 2 bits = 10 bits

La somme des octets codés est 12 + 3 + 12 + 6 + 10 = 43 bits

Ajoutez cela aux 49 bits de l’arbre et la sortie sera de 92 bits, soit 12 octets. Comparez cela aux 20 * 8 octets nécessaires pour stocker les 20 caractères originaux non codés, vous économiserez 8 octets.

Le résultat final, y compris l'arbre pour commencer, est le suivant. Chaque caractère du flux (A-E) est codé sur 8 bits, tandis que 0 et 1 ne représentent qu'un seul bit. L'espace dans le flux sert uniquement à séparer l'arborescence des données codées et n'occupe aucun espace dans la sortie finale.

001A1C01E01B1D 0000000000001100101010101011111111010101010

Pour l'exemple concret que vous avez dans les commentaires, AABCDEF, vous obtiendrez ceci:

Entrée: AABCDEF

Fréquences:

  • A: 2
  • B: 1
  • C: 1
  • D: 1
  • E: 1
  • F: 1

Arbre:

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

Chemins:

  • A: 00
  • B: 01
  • C: 100
  • D: 101
  • E: 110
  • F: 111

Arbre: 001A1B001C1D01E1F = 59 bits
Données: 000001100101110111 = 18 bits
Somme: 59 + 18 = 77 bits = 10 octets

Étant donné que l'original contenait 7 caractères de 8 bits = 56, vous aurez trop de temps système par rapport à ces petits fichiers.

Si vous avez suffisamment de contrôle sur la génération de l'arborescence, vous pouvez lui faire créer une arborescence canonique (de la même manière DEFLATE ne, par exemple), ce qui signifie que vous créez des règles pour résoudre toute situation ambiguë lors de la construction de l'arborescence. . Ensuite, comme DEFLATE, tout ce que vous avez à stocker est la longueur des codes de chaque caractère.

Autrement dit, si vous aviez les arbres/codes Lasse mentionnés ci-dessus:

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

Ensuite, vous pouvez les stocker comme: 2, 3, 2, 3, 2

Et c’est en fait assez d’informations pour régénérer la table de Huffman, en supposant que vous utilisez toujours le même jeu de caractères - par exemple, ASCII. (Ce qui signifie que vous ne pouvez pas sauter de lettres - vous auriez à lister une longueur de code pour chacune, même si c'est zéro.)

Si vous définissez également une limite de longueur de bits (par exemple, 7 bits), vous pouvez stocker chacun de ces nombres en utilisant des chaînes binaires courtes. Ainsi, 2,3,2,3,2 devient 010 011 010 011 010 - Ce qui correspond à 2 octets.

Si vous voulez devenir vraiment fou, vous pouvez faire ce que DEFLATE fait, créer un autre tableau de Huffman des longueurs de ces codes et stocker au préalable ses longueurs de code. D'autant plus qu'ils ajoutent des codes supplémentaires pour "insérer zéro N fois de suite" afin de raccourcir davantage les choses.

La RFC pour DEFLATE n’est pas si mauvaise, si vous connaissez déjà le codage de Huffman: http://www.ietf.org/rfc/rfc1951.txt

9
Ezran

branches sont 0 feuilles sont 1. Traversez la profondeur de l'arbre en premier pour obtenir sa "forme"

e.g. the shape for this tree

0 - 0 - 1 (A)
|    \- 1 (E)
  \
    0 - 1 (C)
     \- 0 - 1 (B)
         \- 1 (D)

would be 001101011

Suivez cela avec les bits pour les caractères de la même profondeur, premier ordre AECBD (lors de la lecture, vous saurez combien de caractères il faut attendre de la forme de l’arbre). Puis sortez les codes pour le message. Vous avez ensuite une longue série de bits que vous pouvez diviser en caractères pour la sortie.

Si vous le découpez, vous pouvez vérifier que le stockage de l’arbre pour le prochain mandrin est aussi efficace que la simple réutilisation de l’arbre pour le bloc précédent et que la forme de l’arbre est "1" comme indicateur pour réutiliser simplement l’arbre du bloc précédent. .

4
Sam Hasler

L'arbre est généralement créé à partir d'une table de fréquence des octets. Alors stockez cette table, ou simplement les octets eux-mêmes triés par fréquence, et recréez l’arbre à la volée. Bien entendu, cela suppose que vous construisez l’arbre pour représenter des octets simples, pas des blocs plus volumineux.

UPDATE: Comme l'a souligné j_random_hacker dans un commentaire, vous ne pouvez pas le faire: vous avez besoin des valeurs de fréquence elles-mêmes. Ils sont combinés et "bouillonnent" vers le haut lorsque vous construisez l’arbre. Cette page décrit la manière dont une arborescence est construite à partir du tableau de fréquences. En prime, cela évite également l’effacement de cette réponse en mentionnant un moyen de sauver l’arbre:

Le moyen le plus simple de générer l’arbre de Huffman lui-même consiste à, en partant de la racine, vider d’abord le côté gauche, puis le côté droit. Pour chaque nœud, vous indiquez un 0, pour chaque feuille, un 1 suivi de N bits représentant la valeur.

2
unwind

Une meilleure approche

Arbre: 7 ------------- | 4 | --------- 3 2 2 ----- ----- ----- A B C D E F 2 1 1 1 1 1 1: fréquences 2 2 3 3 3 3: profondeur de l'arbre (bits de codage)

Maintenant dérivez ce tableau: profondeur nombre de codes


 2   2 [A B]
 3   4 [C D E F]

Vous n'avez pas besoin d'utiliser le même arbre binaire, il vous suffit de conserver la profondeur d'arbre calculée, c'est-à-dire le nombre de bits d'encodage. Il suffit donc de garder le vecteur des valeurs non compressées [A B C D E F] ordonné par la profondeur de l’arbre, utilisez plutôt des index relatifs à ce vecteur séparé. Maintenant, recréez les modèles de bits alignés pour chaque profondeur:

profondeur nombre de codes


 2   2 [00x 01x]
 3   4 [100 101 110 111]

Ce que vous voyez immédiatement, c’est que seul le premier motif binaire de chaque ligne est significatif. Vous obtenez la table de consultation suivante:

first pattern depth first index
------------- ----- -----------
000           2     0
100           3     2

Cette LUT a une très petite taille (même si vos codes de Huffman peuvent être longs de 32 bits, elle ne contiendra que 32 lignes), et en fait le premier motif est toujours nul, vous pouvez l'ignorer complètement lors d'une recherche binaire de motifs. dedans (ici seulement un motif devra être comparé pour savoir si la profondeur de bits est 2 ou 3 et obtenir le premier index auquel les données associées sont stockées dans le vecteur). Dans notre exemple, vous devez effectuer une recherche binaire rapide des modèles d’entrée dans un espace de recherche de 31 valeurs au plus, c’est-à-dire qu’un maximum de 5 entiers est comparé. Ces 31 routines de comparaison peuvent être optimisées en 31 codes pour éviter toutes les boucles et la gestion des états lors du brassage de l'arbre de recherche binaire entier. Toute cette table tient dans une petite longueur fixe (la LUT nécessite seulement 31 lignes au maximum pour les codes de Huffman ne dépassant pas 32 bits, et les 2 autres colonnes ci-dessus rempliront au maximum 32 lignes).

En d'autres termes, la table de conversion ci-dessus nécessite 31 pouces de taille 32 bits chacun, 32 octets pour stocker les valeurs de profondeur de bits: vous pouvez toutefois l'éviter en impliquant la colonne de profondeur (et la première ligne pour la profondeur 1):

first pattern (depth) first index
------------- ------- -----------
(000)          (1)    (0)
 000           (2)     0
 100           (3)     2
 000           (4)     6
 000           (5)     6
 ...           ...     ...
 000           (32)    6

Donc, votre LUT contient [000, 100, 000 (30 fois)]. Pour y effectuer une recherche, vous devez trouver la position où le motif de bits d’entrée se situe entre deux motifs: il doit être inférieur au motif situé à la position suivante dans cette LUT, mais toujours supérieur ou égal au motif de la position actuelle (si les deux positions sont identiques). contient le même motif, la ligne en cours ne correspondra pas, le motif en entrée s’ajuste en dessous). Vous allez ensuite diviser et conquérir, et vous utiliserez au plus 5 tests (la recherche binaire nécessite un seul code avec 5 niveaux imbriqués si/alors/sinon, elle a 32 branches, la branche atteinte indique directement la profondeur de bits qui ne doivent être stockés; vous effectuez ensuite une recherche directement indexée dans la seconde table pour renvoyer le premier index; vous dérivez de manière additive l’index final dans le vecteur de valeurs décodées).

Une fois que vous avez trouvé une position dans la table de recherche (recherchez dans la 1ère colonne), vous avez immédiatement le nombre de bits à extraire de l'entrée, puis l'index de départ du vecteur. La profondeur de bits obtenue peut être utilisée pour déduire directement la position d'index ajustée, par masquage de base, après soustraction du premier index.

En résumé: ne stockez jamais les arbres binaires liés, et vous n’avez pas besoin de boucle pour effectuer la recherche nécessitant seulement 5 if imbriqués comparant des patterns à des positions fixes dans une table de 31 patterns, et une table de 31 ints contenant le décalage de début dans le vecteur de valeurs décodées (dans la première branche des tests imbriqués if/then/else, le décalage de début par rapport au vecteur est implicite, il est toujours égal à zéro; il s'agit également de la branche la plus fréquente qui sera prise car elle correspond au code le plus court qui est pour les valeurs décodées les plus fréquentes).

0
verdy_p