Le codage de Huffman
Présentation rapide⚓︎
Le codage de Huffman est un codage qui permet de compresser des données. Dans le cas d'un texte, codé en UTF-8, chaque caractère prendra la place de un à 4 octets. Dans le cas du codage de Huffman , plus un caractère est fréquent dans le texte, plus court sera son codage en binaire.
Exemple
Le texte : que voulez vous que je vous dise
sera codé par :
01111011101110000011011000011010001111000001101010111011110111011110010110111000001101010111100110110010110.
Le caractère espace sera codé par 111, tandis que le caractère l sera codé par 1 0000.
Note
Toutes les fonctions créées dans des questions, sont utilisables dans les questions suivantes.
Déterminer le nombre d'occurrences⚓︎
Exemple
Prenons pour exemple le texte que voulez vous que je vous dise.
Le nombre d'occurrences de chaque lettre est :
| q | u | e | v | o | l | z | s | j | d | i | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 2 | 5 | 5 | 6 | 3 | 3 | 1 | 1 | 3 | 1 | 1 | 1 |
Question 1 : compter_occurrences(chaine)
Écrire la fonction compter_occurrences qui prend en paramètre une chaîne de caractères, et qui renvoie un dictionnaire dont les clés sont les caractères et les valeurs associées le nombre d'occurrences correspondant. La chaîne de caractères est non vide.
Exemple
>>> compter_occurrences("que voulez vous que je vous dise")
{'q':2,'u':5, 'e':5, ' ':6, 'v':3, 'o':3,'l':1, 'z':1, 's':3, 'j':1, 'd':1, 'i':1}
Trier le dictionnaire des occurrences⚓︎
Le codage de Huffman , est basé sur un arbre construit à partir des caractères et du nombre d'occurrences.
Pour cette construction, il est nécessaire d'avoir une liste triée, selon le poids croissant des différentes lettres.
Question 2 : trier_occurrences(dico_occurrences)
Créer la fonction trier_occurrences, qui prend un paramètre, un dictionnaire comme ci-dessus, obtenu comme retour de la fonction compter_occurrences, et qui renvoie une liste triée de dictionnaires dont les clés sont 'caractère' associée au caractère et 'poids' associée au nombre d'occurrences de ce caractère.
Exemple
>>> trier_occurrences({'q': 2, 'u': 5, 'e': 5, ' ': 6, 'v': 3, 'o': 3, 'l': 1, 'z': 1, 's': 3, 'j': 1, 'd': 1, 'i': 1})
[{'caractere': 'l', 'poids': 1}, {'caractere': 'z', 'poids': 1}, {'caractere': 'j', 'poids': 1}, {'caractere': 'd', 'poids': 1}, {'caractere': 'i', 'poids': 1}, {'caractere': 'q', 'poids': 2}, {'caractere': 'v', 'poids': 3}, {'caractere': 'o', 'poids': 3}, {'caractere': 's', 'poids': 3}, {'caractere': 'u', 'poids': 5}, {'caractere': 'e', 'poids': 5}, {'caractere': ' ', 'poids': 6}]
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Construction de l'arbre de Huffman⚓︎
L'arbre de Huffman est un arbre construit progressivement à partir des feuilles.
Voici un exemple de construction :
Exemple
On commence par créer des nœuds pour tous les caractères. La donnée associée à chaque nœud est un caractère et son poids qui est son nombre d'occurrences.
graph TD
N7(l:1):::feuille
N8(z:1):::feuille
N90(j:1):::feuille
N10(d:1):::feuille
N1(i:1):::feuille
N11(q:2):::feuille
N5(v:3):::feuille
N6(o:3):::feuille
N9(s:3):::feuille
N2(u:5):::feuille
N3(e:5):::feuille
N4(_:6):::feuille
classDef feuille fill:#F88
À partir des deux nœuds ayant les plus petits poids, on crée un arbre binaire :
- la racine aura pour donnée un symbole vide et comme poids la somme des poids des deux nœuds.
- des sous arbres droit et gauche qui seront les deux nœuds sélectionnés.
Ici les nœuds l:1 et z:1 ont été fusionnés en un nouvel arbre dont le poids est 1 + 1 = 2
graph TD
N90(j:1):::feuille
N10(d:1):::feuille
N1(i:1):::feuille
N11(q:2):::feuille
N5(v:3):::feuille
N6(o:3):::feuille
N9(s:3):::feuille
N2(u:5):::feuille
N3(e:5):::feuille
N4(_:6):::feuille
N13( :2) --> N7(l:1):::feuille
N13 --> N8(z:1):::feuille
classDef feuille fill:#F88
Parmi l'ensemble des nœuds, on choisit les deux nœuds de poids le plus faible, pour créer un arbre dont la racine aura pour donnée la somme des poids des deux nœuds et, pour sous arbres gauche et droit, les nœuds choisis.
graph TD
N1(i:1):::feuille
N11(q:2):::feuille
N5(v:3):::feuille
N6(o:3):::feuille
N9(s:3):::feuille
N2(u:5):::feuille
N3(e:5):::feuille
N4(_:6):::feuille
N12( :2) --> N7(l:1):::feuille
N12 --> N8(z:1):::feuille
N13( :2) --> N90(j:1):::feuille
N13 --> N10(d:1):::feuille
classDef feuille fill:#F88
On poursuit jusqu'à ce qu'il ne reste qu'un seul arbre dont le poids sera égal à la somme des poids des nœuds initiaux, c'est à dire à la longueur du message.
graph TD
N5(v:3):::feuille
N6(o:3):::feuille
N9(s:3):::feuille
N2(u:5):::feuille
N3(e:5):::feuille
N4(_:6):::feuille
N12( :2) --> N7(l:1):::feuille
N12 --> N8(z:1):::feuille
N13( :2) --> N90(j:1):::feuille
N13 --> N10(d:1):::feuille
N14( :3) --> N1(i:1):::feuille
N14 --> N11(q:2):::feuille
classDef feuille fill:#F88
On poursuit jusqu'à ce qu'il ne reste qu'un seul arbre dont le poids sera égal à la somme des poids des nœuds initiaux, c'est à dire à la longueur du message. Ici on a fusionné deux arbres déjà construits car leur poids (2) étaient plus petits que celui des feuilles (3 au minimum.)
graph TD
N5(v:3):::feuille
N6(o:3):::feuille
N9(s:3):::feuille
N2(u:5):::feuille
N3(e:5):::feuille
N4(_:6):::feuille
N14( :3) --> N1(i:1):::feuille
N14 --> N11(q:2):::feuille
N15( :4) --> N12( :2)
N15 --> N13( :2)
N12( :2) --> N7(l:1):::feuille
N12 --> N8(z:1):::feuille
N13( :2) --> N90(j:1):::feuille
N13 --> N10(d:1):::feuille
classDef feuille fill:#F88
graph TD
N9(s:3):::feuille
N2(u:5):::feuille
N3(e:5):::feuille
N4(_:6):::feuille
N14( :3) --> N1(i:1):::feuille
N14 --> N11(q:2):::feuille
N15( :4) --> N12( :2)
N15 --> N13( :2)
N12 --> N7(l:1):::feuille
N12 --> N8(z:1):::feuille
N13 --> N90(j:1):::feuille
N13 --> N10(d:1):::feuille
N16( :6) --> N5(v:3):::feuille
N16 --> N6(o:3):::feuille
classDef feuille fill:#F88
Ici les deux nœuds aux poids les plus petits, sont la feuille s:3 et l'arbre qui avait un poids de 3
graph TD
N2(u:5):::feuille
N3(e:5):::feuille
N4(_:6):::feuille
N15( :4) --> N12( :2)
N15 --> N13( :2)
N12 --> N7(l:1):::feuille
N12 --> N8(z:1):::feuille
N13 --> N90(j:1):::feuille
N13 --> N10(d:1):::feuille
N16( :6) --> N5(v:3):::feuille
N16 --> N6(o:3):::feuille
N17( :6) --> N9(s:3):::feuille
N17 --> N14( :3)
N14 --> N1(i:1):::feuille
N14 --> N11(q:2):::feuille
classDef feuille fill:#F88
graph TD
N3(e:5):::feuille
N4(_:6):::feuille
N16( :6) --> N5(v:3):::feuille
N16 --> N6(o:3):::feuille
N17( :6) --> N9(s:3):::feuille
N17 --> N14( :3)
N14 --> N1(i:1):::feuille
N14 --> N11(q:2):::feuille
N18( :9) --> N15( :4)
N18 --> N2(u:5):::feuille
N15 --> N12( :2)
N15 --> N13( :2)
N12 --> N7(l:1):::feuille
N12 --> N8(z:1):::feuille
N13 --> N90(j:1):::feuille
N13 --> N10(d:1):::feuille
classDef feuille fill:#F88
graph TD
N16( :6) --> N5(v:3):::feuille
N16 --> N6(o:3):::feuille
N17( :6) --> N9(s:3):::feuille
N17 --> N14( :3)
N14 --> N1(i:1):::feuille
N14 --> N11(q:2):::feuille
N18( :9) --> N15( :4)
N18 --> N2(u:5):::feuille
N15 --> N12( :2)
N15 --> N13( :2)
N12 --> N7(l:1):::feuille
N12 --> N8(z:1):::feuille
N13 --> N90(j:1):::feuille
N13 --> N10(d:1):::feuille
N19( :11) --> N3(e:5):::feuille
N19 --> N4(_:6):::feuille
classDef feuille fill:#F88
graph TD
N18( :9) --> N15( :4)
N18 --> N2(u:5):::feuille
N15 --> N12( :2)
N15 --> N13( :2)
N12 --> N7(l:1):::feuille
N12 --> N8(z:1):::feuille
N13 --> N90(j:1):::feuille
N13 --> N10(d:1):::feuille
N19( :11) --> N3(e:5):::feuille
N19 --> N4(_:6):::feuille
N20( :12) --> N16( :6)
N20 --> N17( :6)
N16 --> N5(v:3):::feuille
N16 --> N6(o:3):::feuille
N17 --> N9(s:3):::feuille
N17 --> N14( :3)
N14 --> N1(i:1):::feuille
N14 --> N11(q:2):::feuille
classDef feuille fill:#F88
graph TD
N20( :12)
N21( :20)
N20 --> N16( :6)
N20 --> N17( :6)
N16 --> N5(v:3):::feuille
N16 --> N6(o:3):::feuille
N17 --> N9(s:3):::feuille
N17 --> N14( :3)
N14 --> N1(i:1):::feuille
N14 --> N11(q:2):::feuille
N21 --> N18( :9)
N18 --> N15( :4)
N18 --> N2(u:5):::feuille
N15 --> N12( :2)
N15 --> N13( :2)
N12 --> N7(l:1):::feuille
N12 --> N8(z:1):::feuille
N13 --> N90(j:1):::feuille
N13 --> N10(d:1):::feuille
N21 --> N19( :11)
N19 --> N3(e:5):::feuille
N19 --> N4(_:6):::feuille
classDef feuille fill:#F88
graph TD
N22( :32) --> N20( :12)
N22 --> N21( :20)
N20 --> N16( :6)
N20 --> N17( :6)
N16 --> N5(v:3):::feuille
N16 --> N6(o:3):::feuille
N17 --> N9(s:3):::feuille
N17 --> N14( :3)
N14 --> N1(i:1):::feuille
N14 --> N11(q:2):::feuille
N21 --> N18( :9)
N18 --> N15( :4)
N18 --> N2(u:5):::feuille
N15 --> N12( :2)
N15 --> N13( :2)
N12 --> N7(l:1):::feuille
N12 --> N8(z:1):::feuille
N13 --> N90(j:1):::feuille
N13 --> N10(d:1):::feuille
N21 --> N19( :11)
N19 --> N3(e:5):::feuille
N19 --> N4(_:6):::feuille
classDef feuille fill:#F88
Pour créer l'arbre, il faut donc créer à partir du dictionnaire des occurrences, une liste ordonnée en fonction du poids du caractère.
L'algorithme de création de l'arbre est le suivant :
Tant qu'il reste au moins deux nœuds:
prendre un nœud de poids minimal et le retirer de l'ensemble des nœuds
prendre un second nœud de poids minimal parmi les nœuds restant et le retirer de l'ensemble des nœuds
créer un nouveau nœud (sans symbole) et de poids la somme des 2 poids des nœuds précédents
ajouter ce nœud à l'ensemble des nœuds
On peut remarquer que lorsqu'on fusionne deux nœuds, le nouveau poids est obligatoirement supérieur ou égal au poids des nœuds qui ont déjà étaient fusionnés.
Si on considère une file_1, construite à partir de la liste des caractères triée par rapport à leur poids, et une seconde file_2, des nœuds fusionnés au fur et à mesure, les deux prochains nœuds à fusionnés seront :
- Le plus petit des deux nœuds en tête des deux files.
- Le plus petit des deux nœuds en tête des deux files, après avoir défilé le premier.
- Si une file est vide, le plus petit est le premier de l'autre file.
On va donc définir :
- une première file
file_caracteresconstruit avec les nœuds(feuilles) obtenus à partir de la liste ordonnée des caractères en fonction du poids, - une seconde file
file_fusionnesqui se remplira au fur et à mesure avec les nœuds fusionnés à partir de deux autres.
On fournit la classe Arbre dont un exemple d'utilisation est donné ci-dessous.
Exemple d'utilisation de la classe Arbre
La classe d'arbre binaire proposée permet :
Constructeur : ArbreBinaire()
Fonction incluse qui permet de créer une feuille directement : feuille(donnee)
Méthodes fournies :
- est_vide()
- get_donnee()
- get_sous_arbre_gauche()
- get_sous_arbre_droit()
- est_feuille()
Exemple d'utilisation
>>> t1 = ArbreBinaire()
>>> t1.est_vide()
True
>>> t2 = ArbreBinaire(1, t1, t1)
>>> t2.est_vide()
False
>>> t2.get_donnee()
1
>>> t2.get_sous_arbre_gauche().est_vide()
True
>>> t2.get_sous_arbre_droit().est_vide()
True
>>> t2.est_feuille()
True
>>> ArbreBinaire("coucou",ArbreBinaire(), ArbreBinaire()) == feuille("coucou")
True
{"caractere":…, "poids":…}, sous forme classique pour visualiser le contenu comme dans les exemples ci-dessus.
>>> a1 =feuille({"caractere":"a","poids":1})
>>> a2 =feuille({"caractere":"b","poids":2})
>>> a3 = ArbreBinaire({"caractere":"","poids":3},a1,a2)
>>> a3.affiche()
':3'
/ \
'a:1' 'b:2'
On fournit la structure de file avec la classe File dont un exemple d'utilisation est donné ci-dessous.
Exemple d'utilisation de la classe File
Afin d'effectuer la comparaison, sans avoir à défiler les éléments, on ajoute la méthode sommet, qui renvoie le prochain élément qui sera défilé, sans le retirer.
Les méthodes :
- constructeur : File()
- est_vide()
- enfiler(x)
- defiler() : renvoie le premier élément de la file
- sommet() : renvoie le sommet de la file, sans le défiler.
La fonction len() renvoie la longueur de la file passée en paramètre.
Exemple d'utilisation
>>> f1 = File()
>>> f1.est_vide()
True
>>> f1.enfiler(10)
>>> f1.enfiler(20)
>>> len(f1)
2
>>> f1.sommet()
10
>>> len(f1)
2
>>> f1.defiler()
10
>>> len(f1)
1
>>> f1.defiler()
20
Question 3 : construire_file_caracteres(liste_triee_occurrences_triees)
Créer la fonction construire_file_caracteres qui prend comme argument une liste de dictionnaires occurrences_triees, résultat de la fonction trier_occurrences, dont les clés sont caractère et poids, et qui renvoie une file qui contient des feuilles dont les données sont les dictionnaires de la liste, rangées dans le même ordre.
Exemple de format des paramètres et du résultat renvoyé
- paramètre :
occurrences_triees = [{'caractere': 'l', 'poids': 1}, {'caractere': 'z', 'poids': 1}, {'caractere': 'v', 'poids': 3}] - retour : File qui contient dans l'ordre
feuille({'caractere': 'l', 'poids': 1}),feuille({'caractere': 'z', 'poids': 1}),feuille({'caractere': 'v', 'poids': 3})
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Question 4 : plus_leger(file1, file2)
Créer la fonction plus_leger qui prend comme arguments deux files, dont les éléments sont des dictionnaires {'caractère': valeur, 'poids': valeur} et renvoie l'élément avec le poids le plus faible, en le défilant. Si l'une des files est vide, c'est le sommet de l'autre file qui est renvoyé. On supposera qu'au moins une des deux files n'est pas vide.
Exemple de format des paramètres et du résultat renvoyé
- paramètre : Deux files qui contiennent des nœuds dont la donnée de la racine est un dictionnaire
{'caractere': …, 'poids': …} - retour : un nœud dont dont la donnée de la racine est un dictionnaire
{'caractere': …, 'poids': …}
Aide pour créer ses tests
Pour créer des files avec des nœuds.
Créer des feuilles, éventuellement des arbres puis enfiler les éléments dans des files.
feuille1 = feuille({'caractere': 'l', 'poids': 1})
feuille2 = feuille({'caractere': 'o', 'poids': 3})
file1 = File()
file1.enfiler(feuille1)
file2.enfiler(feuille2)
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Question 5 : construire_arbre(file)
Créer la fonction construire_arbre qui prend comme argument une file file_caracteres, comme ci-dessus et renvoie un arbre de Huffman
construit avec la méthode décrite ci-dessus.
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Question 6 : arbre_Huffman(chaine)
Enfin, créer une fonction arbre_Huffman qui prend comme argument une chaîne de caractères et qui renvoie un arbre de Huffman.
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Encodage⚓︎
Une fois l'arbre construit, il faut procéder à l'encodage du texte. Pour cela on va créer un dictionnaire, dont les clés seront les caractères et les valeurs associées le codage binaire du caractère en utilisant l'arbre construit.
Pour cela on va utiliser la convention suivante :
Dans l'arbre chaque embranchement à gauche correspond à 0 et chaque embranchement à droite correspond à 1.
Exemple d'arbre et de codes
On obtient l'arbre suivant :
graph TD
N22( :32) --> |0| N20( :12)
N22 --> |1| N21( :20)
N20 --> |0| N16( :6)
N20 --> |1| N17( :6)
N16 --> |0| N5(v:3):::feuille
N16 --> |1| N6(o:3):::feuille
N17 --> |0| N9(s:3):::feuille
N17 --> |1| N14( :3)
N14 --> |0| N1(i:1):::feuille
N14 --> |1| N11(q:2):::feuille
N21 --> |0| N18( :9)
N21 --> |1| N19( :11)
N18 --> |0| N15( :4)
N18 --> |1| N2(u:5):::feuille
N15 --> |0| N12( :2)
N15 --> |1| N13( :2)
N12 --> |0| N7(l:1):::feuille
N12 --> |1| N8(z:1):::feuille
N13 --> |0| N90(j:1):::feuille
N13 --> |1| N10(d:1):::feuille
N19 --> |0| N3(e:5):::feuille
N19 --> |1| N4(_:6):::feuille
classDef feuille fill:#F88
graph TD
N22( :32) --> |0| N20( :12)
N22 --> |1| N21( :20)
N20 --> |0| N16( :6)
N20 --> |1| N17( :6)
N16 --> |0| N5(v:3):::feuille
N16 --> |1| N6(o:3):::feuille
N17 --> |0| N9(s:3):::feuille
N17 --> |1| N14( :3)
N14 --> |0| N1(i:1):::feuille
N14 --> |1| N11(q:2):::feuille
N21 --> |0| N18( :9)
N21 --> |1| N19( :11)
N18 --> |0| N15( :4)
N18 --> |1| N2(u:5):::feuille
N15 --> |0| N12( :2)
N15 --> |1| N13( :2)
N12 --> |0| N7(l:1):::feuille
N12 --> |1| N8(z:1):::feuille
N13 --> |0| N90(j:1):::feuille
N13 --> |1| N10(d:1):::feuille
N19 --> |0| N3(e:5):::feuille
N19 --> |1| N4(_:6):::feuille
N5 o-...-o N32[000]:::binaire
N1 o-..-o N33[0110]:::binaire
N4 o-...-o N30[111]:::binaire
N7 o-.-o N31[1 0000]:::binaire
classDef feuille fill:#F88
classDef binaire fill:#FF0
Par exemple, l (peu fréquent) sera codé 1 0000 (5 bits) alors que l'espace (très fréquente) sera codée 111 (3 bits).
Plus un symbole est fréquent, plus il est proche de la racine :
Question 7 : lettre_binaire(arbre)
Créer la fonction lettre_binaire qui prend comme argument un arbre de Huffman
et qui renvoie un dictionnaire avec comme clé, les caractères et comme valeurs associées, le codage binaire du caractère.
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Question 8 : encoder_texte(texte)
Il faut maintenant encoder le texte initial avec ce code obtenu.
Créer la fonction encoder_texte qui prend comme argument un texte et qui renvoie une chaîne de caractères contenant le codage binaire du texte.
L'arbre de Huffman est créer au début de la fonction, ainsi que le dictionnaire de codage.
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Remarque
Pour l'exercice le codage est « textualisé », alors que, dans la réalité, l'opération se passe au niveau des octets et est donc plus difficilement visible.
Décodage⚓︎
Question 9 : decoder_texte(texte, arbre)
Créer la fonction decoder_texte qui prend comme argument un texte codé et l'arbre de Huffman
, et qui renvoie une chaîne de caractères contenant le texte décodé.
Le décodage du texte se fait en parcourant l'arbre. On s'interdit d'utiliser et ou de reconstruire le dictionnaire de codage.
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Remarque
En pratique, le dictionnaire de codage est transmis dans le fichier avec le texte à décoder. Il faut d'abord reconstruire l'arbre, avant de décoder les données.
# Tests(insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)