Codage de Huffman (construction de l'arbre)
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 à quatre 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.
Soit \(107\) bits au lieu de \(256\) avec le codage UTF-8. Le caractère espace ' ' sera codé par 111, tandis que le caractère 'l' sera codé par 10000.
Ce codage est basé sur la construction d'un arbre, objectif de cet exercice.
Note
Toutes les fonctions créées dans des questions sont utilisables dans les questions suivantes.
Principe de construction⚓︎
- Le codage de Huffman est basé sur un arbre, dont les feuilles sont liées aux caractères du texte.
- On détermine les occurrences de tous les caractères d'un texte.
- On crée les feuilles du futur arbre. Chaque feuille aura pour étiquette un caractère et un poids qui est son nombre d'occurrences dans le texte.
- On fusionne les feuilles et les nœuds deux par deux.
- Chaque fusion produit un nouvel arbre dont le nœud racine a pour étiquette un poids égal à la somme des poids des étiquettes des nœuds fusionnés.
Exemple de construction de l'arbre
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 |
On peut alors créer les feuilles du futur arbre.
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
Ces feuilles sont triées selon l'ordre croissant du poids de leur nœud.
L'arbre de Huffman est un arbre construit progressivement à partir des feuilles.
À partir des deux nœuds ayant les plus petits poids, on crée un arbre :
- la racine aura pour donnée un symbole vide et comme poids la somme des poids des deux nœuds ;
- les sous arbres droit et gauche seront les deux nœuds sélectionnés.
Ici les feuilles l:1 et z:1 ont été fusionnés de façon à former un nouvel arbre dont le poids de la racine 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
classDef feuille fill:#F88
graph TD
N13(( :2)) --> N7(l:1):::feuille
N13 --> N8(z:1):::feuille
classDef feuille fill:#F88
Parmi l'ensemble des nœuds, d'une part les feuilles et d'autre part les nœuds fusionnés, on choisit les deux nœuds de poids les plus faibles, 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
classDef feuille fill:#F88
graph TD
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 nœud. Lorsqu'une feuille et un nœud ont le même poids, on privilégie la feuille.
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
classDef feuille fill:#F88
graph TD
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
Ici on a fusionné deux nœuds 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
classDef feuille fill:#F88
graph TD
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
classDef feuille fill:#F88
graph TD
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 le nœud construit, qui avait un poids de 3
graph TD
N2(u:5):::feuille
N3(e:5):::feuille
N4(_:6):::feuille
classDef feuille fill:#F88
graph TD
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
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
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
On poursuit jusqu'à ce qu'il ne reste qu'un seul nœud dont le poids sera égal à la somme des poids des nœuds initiaux, c'est à dire à la longueur du message.
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 la suite, on adaptera l'algorithme en utilisant la structure suivante :
- chaque nœud est un arbre dont l'étiquette de la racine sera un dictionnaire dont les clés seront
caractèreetpoids. - une file, nommée par la suite
file1, contiendra l'ensemble des feuilles enfilées dans l'ordre croissant de leur poids. - une file, nommée par la suite
file2, contiendra l'ensemble des nœuds fusionnés au fur et à mesure. - si au début, la file contenant les feuilles, ne comporte qu'une seule feuille, l'arbre de Huffman correspondant est cette feuille.
- la file contenant les feuilles sera systématiquement vide à la fin du traitement.
L'algorithme adapté à la structure
Tant qu'il reste au moins deux nœuds dans l'ensemble des deux files:
- prendre les deux nœuds de poids minimal et les retirer de leur file. À poids égaux, les nœuds des dictionnaires de `file1`
sont choisis prioritairement.
- fusionner les deux nœuds (avec l'étiquette `caractère` égale à `""` et le poids égal à la somme des 2 poids des nœuds précédents)
- enfiler ce nœud à la file 2 des nœuds fusionnés
Renvoyer l'unique nœud, correspondant à l'arbre recherché.
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à été fusionnés. La seconde file est donc toujours ordonnée de façon croissante.
Éléments fournis
-
Une classe
Arbredont un exemple d'utilisation est donné ci-dessous.La classe
ArbreLa classe d'arbre proposée dispose de l'interface suivante :
Constructeur :
Arbre(donnee): crée un arbre dont la racine a pour étiquettedonnee, qui ici sera un dictionnaire.Arbre(donnee, sous_arbre_gauche, sous_arbre_droit): crée un arbre dont la racine a l'étiquettedonneeet deux sous-arbressous_arbre_gaucheetsous_arbre_droit
Méthodes fournies :
donnee(): renvoie l'étiquette de la racinesous_arbre_gauche(): renvoie le sous-arbre droitsous_arbre_droit(): renvoie le sous-arbre gaucheest_feuille()
Exemple d'utilisation
On ajoute la possibilité d'afficher un arbre, dont les étiquettes sont de forme
{"caractere":…, "poids":…}, sous forme classique pour visualiser le contenu.🐍 Console Python>>> a1 = Arbre({"caractere": "a", "poids": 1}) >>> a1.est_feuille() True >>> a1.donnee() {'caractere': 'a', 'poids': 1} >>> a2 = Arbre({"caractere": "b", "poids": 2}) >>> a3 = Arbre({"caractere": "", "poids": 3}, a1, a2) >>> a3.affiche() ':3' / \ 'a:1' 'b:2' -
Une structure de file avec la classe
Filedont un exemple d'utilisation est donné ci-dessous.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.Le constructeur :
File()Les méthodes :
est_vide(): renvoie unTruesi la file est vide,Falsesinon.enfile(x): placexà la queue de la filedefile(): renvoie le premier élément de la filesommet(): 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
🐍 Console Python>>> f1 = File() >>> f1.est_vide() True >>> f1.enfile(10) >>> f1.enfile(20) >>> len(f1) 2 >>> f1.sommet() 10 >>> len(f1) 2 >>> f1.defile() 10 >>> len(f1) 1 >>> f1.defile() 20 -
Une fonction
construit_file_caracteresqui prend en paramètre une liste de dictionnaires sous la forme[{'caractere': 'l', 'poids': 1}, {'caractere': 'o', 'poids': 3},...], et qui renvoie une file de feuilles dont l'étiquette est un dictionnaire{'caractère': valeur, 'poids': valeur}. Les éléments de la file sont triés par ordre croissant. L'élément de poids le plus petit sera le premier élément défilé.Exemple d'utilisation
🐍 Console PythonL'objet>>> occurrences_1 = [{'caractere': 'l', 'poids': 1}, {'caractere': 'o', 'poids': 3}] >>> file1 = construit_file_caracteres(occurrences_1)file1contient dans l'ordreArbre({'caractere': 'l', 'poids': 1}),Arbre({'caractere': 'o', 'poids': 3})
Question 1 : plus_leger(file1, file2)
Créer la fonction plus_leger.
Cette fonction prend comme paramètres deux files file1 et file2 , file1 étant une file de feuilles enfilées dans l’ordre croissant des poids et file2 une file de nœuds(éventuellement feuilles) fusionnés enfilés dans l’ordre croissant du poids de la racine. La fonction renvoie l’élément de poids le plus faible de file1 et file2 et le défile.
Les éléments des files sont des feuilles dont l'étiquette est un dictionnaire {'caractère': valeur, 'poids': valeur}, ou des arbres dont l'étiquette de la racine est un dictionnaire {'caractère': valeur, 'poids': valeur}.
Différents cas :
Si l'une des files est vide, c'est le sommet de l'autre file qui est renvoyé.
graph RL
subgraph resul [Plus léger]
R1(l:1):::feuille_resul
end
subgraph sub1 [file 1]
direction TB
N7(l:1):::feuille_resul
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
end
subgraph sub2 [file 2]
direction TB
IN7( ):::invisible
IN8( ):::invisible
IN90( ):::invisible
IN10( ):::invisible
IN1( ):::invisible
IN11( ):::invisible
end
classDef feuille fill:#F88
classDef feuille_resul fill:#FF8
classDef invisible opacity:0
sub1 --> resul
sub2 --> resul
graph RL
subgraph resul [Plus léger]
subgraph noeud0 [ ]
direction TB
IN16(( :6)):::feuille_resul --> IN5(v:3):::feuille
IN16 --> IN6(o:3):::feuille
end
end
subgraph sub1 [file 1]
direction TB
IN7( ):::invisible
IN8( ):::invisible
IN90( ):::invisible
IN10( ):::invisible
IN1( ):::invisible
IN11( ):::invisible
end
subgraph sub2 [file 2]
direction TB
subgraph noeud4 [ ]
direction TB
N19(( :11)) --> N3(e:5):::feuille
N19 --> N4(_:6):::feuille
end
subgraph noeud3 [ ]
direction TB
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
end
subgraph noeud2 [ ]
direction TB
N17(( :6)) --> N9(s:3):::feuille
N17 --> N14(( :3))
N14 --> N1(i:1):::feuille
N14 --> N11(q:2):::feuille
end
subgraph noeud1 [ ]
direction TB
N16(( :6)):::feuille_resul --> N5(v:3):::feuille
N16 --> N6(o:3):::feuille
end
end
classDef feuille fill:#F88
classDef feuille_resul fill:#FF8
classDef invisible opacity:0
sub1 --> resul
sub2 --> resul
C'est le cas général, c'est l'élément au poids le plus petit qui est renvoyé.
graph RL
subgraph resul [Plus léger]
direction TB
IN1(i:1):::feuille_resul
end
subgraph sub1 [file 1]
direction TB
N1(i:1):::feuille_resul
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
end
subgraph sub2 [file 2]
direction TB
subgraph noeud1 [ ]
direction TB
N12(( :2)) --> N7(l:1):::feuille
N12 --> N8(z:1):::feuille
end
subgraph noeud2 [ ]
direction TB
N13(( :2)) --> N90(j:1):::feuille
N13 --> N10(d:1):::feuille
end
end
classDef feuille fill:#F88
classDef feuille_resul fill:#FF8
classDef invisible opacity:0
sub1 --> resul
sub2 --> resul
graph RL
subgraph resul [Plus léger]
direction TB
subgraph noeud3 [ ]
direction TB
IN13(( :2)) --> IN90(j:1):::feuille
IN13 --> IN10(d:1):::feuille
IN13:::feuille_resul
end
end
subgraph sub1 [file 1]
direction TB
N5(v:3):::feuille
N6(o:3):::feuille
N9(s:3):::feuille
N2(u:5):::feuille
N3(e:5):::feuille
N4(_:6):::feuille
end
subgraph sub2 [file 2]
direction TB
subgraph noeud2 [ ]
direction TB
N12(( :2)) --> N7(l:1):::feuille
N12 --> N8(z:1):::feuille
end
subgraph noeud1 [ ]
direction TB
N13(( :2)) --> N90(j:1):::feuille
N13 --> N10(d:1):::feuille
N13:::feuille_resul
end
end
classDef feuille fill:#F88
classDef feuille_resul fill:#FF8
classDef invisible opacity:0
sub1 --> resul
sub2 --> resul
Si il y a égalité, c'est l'élément de la file 1 qui sera renvoyé.
graph RL
subgraph resul [Plus léger]
direction TB
IN11(q:2):::feuille_resul
end
subgraph sub1 [file 1]
direction TB
N11(q:2):::feuille_resul
N5(v:3):::feuille
N6(o:3):::feuille
N9(s:3):::feuille
N2(u:5):::feuille
N3(e:5):::feuille
N4(_:6):::feuille
end
subgraph sub2 [file 2]
direction TB
subgraph noeud2 [ ]
direction TB
N12(( :2)) --> N7(l:1):::feuille
N12 --> N8(z:1):::feuille
end
subgraph noeud1 [ ]
direction TB
N13(( :2)) --> N90(j:1):::feuille
N13 --> N10(d:1):::feuille
end
end
classDef feuille fill:#F88
classDef feuille_resul fill:#FF8
classDef invisible opacity:0
sub1 --> resul
sub2 --> resul
On assure qu'au moins une des deux files n'est pas vide.
Aide pour créer ses propres tests
Exemple
Si liste_occurrences_triee = [{'caractere': 'l', 'poids': 1}, {'caractere': 'z', 'poids': 1}, {'caractere': 'v', 'poids': 3}]
-
Pour créer des files de feuilles, on fournit la fonction
construit_file_caracteresqui prend comme argument une liste triée de dictionnaires dont les clés sontcaractèreetpoids, 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.L'objetoccurrences_1 = [{'caractere': 'l', 'poids': 1}, {'caractere': 'o', 'poids': 3}] file1 = construit_file_caracteres(occurrences_1)file1contient dans l'ordreArbre({'caractere': 'l', 'poids': 1}),Arbre({'caractere': 'o', 'poids': 3}) -
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 = Arbre({'caractere': 'l', 'poids': 1}) # crée un arbre réduit à sa racine. feuille2 = Arbre({'caractere': 'o', 'poids': 3}) feuille3 = Arbre({'caractere': 'v', 'poids': 3}) arbre1 = Arbre({'caractere': '', 'poids': 4}, feuille1, feuille2) # crée un arbre avec une racine et deux feuilles file1 = File() file1.enfile(arbre1) file2.enfile(feuille3)
Question 2 : construit_arbre(file)
Rappel de l'algorithme de construction de l'arbre.
Tant qu'il reste au moins deux nœuds dans l'ensemble des deux files:
prendre les deux nœuds de poids minimal et les retirer de leur file. À poids égaux, les nœuds de `file1` sont choisis prioritairement.
fusionner les deux nœuds (avec l'étiquette `caractère` égale à `""` et le poids égal à la somme des 2 poids des nœuds précédents)
ajouter ce nœud à la file 2 des nœuds fusionnés
Renvoyer l'unique nœud, correspondant à l'arbre recherché.
Créer la fonction construit_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.
La file file_caracteres est supposée triée. Le premier élément qui sera défilé aura le poids le plus petit.
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Complement arbre_Huffman(chaine)
Pour créer un arbre de Huffman à partir d'une chaîne de caractères, il faut :
- déterminer les occurrences
- créer la file des feuilles
- créer l'arbre.
Voici un IDE qui permet de tester le résultat, avec vos propres chaînes de caractères. La fonction à utiliser est arbre_Huffman(chaine) qui prend en paramètre une chaine de caractères et renvoie un arbre de Huffman construit.
Pour afficher le résultat sous forme d'arbre, utiliser la méthode affiche(). (L'affichage étant textuelle, attention à ne pas mettre un arbre trop grand)
>>> arbre = arbre_Huffman("Coucou !")
>>> arbre.affiche()
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
# Tests(insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)