Cette page suit toujours le même plan : le vocabulaire, la définition, un exemple unique (repris dans toute la page pour ne pas avoir à en retenir plusieurs), l'implémentation en Python avec une classe Noeud, la hauteur, la taille, un aperçu du codage avec un arbre, puis la complexité.
src/arbres_binaires.py, dans le sous-dossier src à côté de cette page, contient tout le code présenté ici (classe Noeud, hauteur, taille, décodage). Après l'avoir téléchargé, on l'exécute dans un terminal, dans le dossier où il a été enregistré, avec python3 arbres_binaires.py (Python doit être installé sur l'ordinateur). Le fichier affiche « OK » pour chaque test s'il n'y a pas d'erreur.
Chaque mot ci-dessous est utilisé exactement dans ce sens dans toute la page.
| Mot | Définition précise |
|---|---|
| Arbre | Une structure de données hiérarchique : soit elle est vide, soit elle est formée d'un élément (un nœud) auquel sont reliés un ou plusieurs sous-arbres. |
| Nœud | Chaque élément de l'arbre. Un nœud contient une valeur, et il est relié à d'autres nœuds. |
| Racine | Le nœud tout en haut de l'arbre, celui qui n'a pas de père. C'est le point de départ pour parcourir tout l'arbre. |
| Branche | Le trait qui relie deux nœuds entre eux. |
| Père | Pour un nœud donné, le nœud juste au-dessus de lui, auquel il est directement relié. |
| Fils | Pour un nœud donné, un nœud directement relié en dessous de lui. Un nœud peut avoir plusieurs fils. |
| Feuille | Un nœud qui n'a aucun fils : c'est un nœud terminal, tout en bas d'une branche. |
| Arbre n-aire | Un arbre où chaque nœud a au plus n fils. |
| Arbre binaire | Un arbre n-aire avec n = 2 : chaque nœud a donc au plus 2 fils. C'est le seul type d'arbre étudié dans cette fiche. |
| Sous-arbre gauche / droit | Dans un arbre binaire, le premier fils d'un nœud (et tout ce qui est relié en dessous de lui) est appelé sous-arbre gauche ; le second, sous-arbre droit. |
| Arbre vide | Un arbre qui ne contient aucun nœud. Dans les diagrammes de cette page, un arbre vide est représenté par une case en pointillés portant le mot « vide ». |
| Hauteur | Le nombre maximum de branches entre la racine et la feuille la plus basse. Par convention, la hauteur d'un arbre vide est -1. |
| Taille | Le nombre total de nœuds de l'arbre. |
Beaucoup d'informations peuvent être organisées sous la forme d'un arbre : un arbre généalogique, une arborescence de fichiers sur un ordinateur, ou encore la structure d'une phrase. Voici un exemple simple, avec un nœud racine qui a trois fils :
Un arbre avec une racine et 3 fils : c'est un exemple d'arbre n-aire (ici, chaque nœud peut avoir jusqu'à 3 fils). « Fils 1 », « Fils 2 » et « Fils 3 » sont ici tous des feuilles, car ils n'ont eux-mêmes aucun fils.
Un arbre binaire est un arbre n-aire avec n = 2 : chaque nœud a donc au plus deux fils, appelés le fils gauche et le fils droit. Le sous-arbre qui part du fils gauche est appelé le sous-arbre gauche ; celui qui part du fils droit, le sous-arbre droit.
Le même arbre binaire, avec 8 nœuds nommés A, G, D, Gp, O, P, Dg et Dd, est repris dans toutes les sections qui suivent : construction avec la classe Noeud, calcul de la hauteur, puis calcul de la taille.
| Nœud | Père | Fils gauche | Fils droit | Feuille ? |
|---|---|---|---|---|
A (racine) | — (aucun) | G | D | non |
G | A | Gp | vide | non |
D | A | Dg | Dd | non |
Gp | G | O | P | non |
O | Gp | vide | vide | oui |
P | Gp | vide | vide | oui |
Dg | D | vide | vide | oui |
Dd | D | vide | vide | oui |
Cet arbre n'est pas symétrique : sa branche gauche (A → G → Gp → O ou P) descend d'un niveau de plus que sa branche droite (A → D → Dg ou Dd). Ce choix est volontaire : il permet de bien voir, plus loin, que la hauteur d'un arbre dépend de sa branche la plus profonde, pas de la moyenne de ses branches.
Noeud| Opération | Effet |
|---|---|
creer_arbre() | crée un arbre binaire vide |
creer_arbre_racine(v) | crée un arbre binaire dont la racine vaut v, avec deux sous-arbres vides |
est_vide(A) | renvoie True si l'arbre A est vide, sinon False |
racine(A) | renvoie la valeur de la racine de A |
sous_arbre_gauche(A) | renvoie le sous-arbre gauche de A |
sous_arbre_droit(A) | renvoie le sous-arbre droit de A |
NoeudOn représente un arbre binaire avec une classe Noeud : chaque objet Noeud a un attribut racine (la valeur du nœud), un attribut gauche (le sous-arbre gauche, ou None s'il est vide) et un attribut droit (le sous-arbre droit, ou None s'il est vide).
class Noeud:
def __init__(self, racine):
self.gauche = None
self.racine = racine
self.droit = None
def __repr__(self):
return f'{self.racine}({str(self.gauche)},{str(self.droit)})'
A.
O et P.Gp, puis relier O comme son fils gauche et P comme son fils droit.G, puis relier Gp comme son fils gauche (son fils droit reste vide, None).Dg et Dd.D, puis relier Dg comme son fils gauche et Dd comme son fils droit.A, puis relier G comme son fils gauche et D comme son fils droit.None)
A n'existe, les fragments d'arbre déjà créés sont affichés côte à côte : ils ne sont pas encore reliés entre eux.O = Noeud('O')P = Noeud('P')Gp = Noeud('Gp')Gp.gauche = OGp.droit = PG = Noeud('G')G.gauche = Gp # G.droit reste vide (None)Dg = Noeud('Dg')Dd = Noeud('Dd')D = Noeud('D')D.gauche = DgD.droit = DdA = Noeud('A')A.gauche = GA.droit = D
Pour se donner une idée de l'arbre construit, on peut afficher A directement (c'est la méthode __repr__ qui est invoquée) :
>>> A A(G(Gp(O(None,None),P(None,None)),None),D(Dg(None,None),Dd(None,None)))
On retrouve, en le lisant, exactement la notation parenthésée de l'arbre : chaque nœud est suivi de ses deux sous-arbres entre parenthèses, dans l'ordre gauche puis droit, et None représente un sous-arbre vide.
On calcule la hauteur en partant des feuilles (les plus faciles, car leurs deux sous-arbres sont vides) et en remontant vers la racine A.
| Nœud | hauteur(sous-arbre gauche) | hauteur(sous-arbre droit) | hauteur du nœud = 1 + max(gauche, droit) |
|---|---|---|---|
O (feuille) | -1 (vide) | -1 (vide) | 1 + max(-1, -1) = 0 |
P (feuille) | -1 (vide) | -1 (vide) | 1 + max(-1, -1) = 0 |
Gp | 0 (hauteur de O) | 0 (hauteur de P) | 1 + max(0, 0) = 1 |
G | 1 (hauteur de Gp) | -1 (vide) | 1 + max(1, -1) = 2 |
Dg (feuille) | -1 (vide) | -1 (vide) | 1 + max(-1, -1) = 0 |
Dd (feuille) | -1 (vide) | -1 (vide) | 1 + max(-1, -1) = 0 |
D | 0 (hauteur de Dg) | 0 (hauteur de Dd) | 1 + max(0, 0) = 1 |
A (racine) | 2 (hauteur de G) | 1 (hauteur de D) | 1 + max(2, 1) = 3 |
La hauteur de l'arbre complet, hauteur(A), vaut donc 3.
La hauteur de chaque nœud est indiquée par le petit badge vert. On voit que le nœud G vaut 2 (et pas 1 + hauteur de Gp + 1) : c'est bien 1 + max(hauteur(Gp), hauteur(vide)) = 1 + max(1, -1) = 2.
def hauteur(self):
if self is None:
return -1
else:
hauteur_gauche = self.gauche.hauteur() if self.gauche else -1
hauteur_droit = self.droit.hauteur() if self.droit else -1
return 1 + max(hauteur_gauche, hauteur_droit)
if self is None ne se déclenche-t-il jamais ici ?Noeud, qui n'est donc jamais None lui-même : le test if self is None protège en réalité les appels récursifs. Comme self.gauche et self.droit peuvent valoir None (sous-arbre vide), on n'appelle .hauteur() dessus que s'ils existent (if self.gauche else -1) ; sinon, on utilise directement -1, la hauteur d'un arbre vide.
| Nœud | taille(sous-arbre gauche) | taille(sous-arbre droit) | taille du nœud = 1 + gauche + droit |
|---|---|---|---|
O (feuille) | 0 (vide) | 0 (vide) | 1 + 0 + 0 = 1 |
P (feuille) | 0 (vide) | 0 (vide) | 1 + 0 + 0 = 1 |
Gp | 1 (taille de O) | 1 (taille de P) | 1 + 1 + 1 = 3 |
G | 3 (taille de Gp) | 0 (vide) | 1 + 3 + 0 = 4 |
Dg (feuille) | 0 (vide) | 0 (vide) | 1 + 0 + 0 = 1 |
Dd (feuille) | 0 (vide) | 0 (vide) | 1 + 0 + 0 = 1 |
D | 1 (taille de Dg) | 1 (taille de Dd) | 1 + 1 + 1 = 3 |
A (racine) | 4 (taille de G) | 3 (taille de D) | 1 + 4 + 3 = 8 |
La taille de l'arbre complet, taille(A), vaut donc 8 : c'est bien le nombre total de nœuds (A, G, D, Gp, O, P, Dg, Dd).
def taille(self):
if self is None:
return 0
else:
taille_gauche = self.gauche.taille() if self.gauche else 0
taille_droit = self.droit.taille() if self.droit else 0
return 1 + taille_gauche + taille_droit
Cette méthode a exactement la même structure que hauteur : un cas de base pour l'arbre vide (0 au lieu de -1), et un appel récursif sur chaque sous-arbre existant.
On peut utiliser la structure d'un arbre binaire pour associer un code fait de 0 et de 1 à chaque feuille : « 0 » quand on emprunte le sous-arbre gauche, « 1 » quand on emprunte le sous-arbre droit. C'est ce principe, avec des arbres plus grands et plus élaborés, qui sera étudié en détail avec le codage de Huffman dans la séquence 9. Voici une version simplifiée, avec 4 fruits codés sur 2 bits chacun :
| Fruit | Chemin depuis la racine | Code |
|---|---|---|
| 🍎 Pomme | gauche, gauche | 00 |
| 🍐 Poire | gauche, droite | 01 |
| 🍌 Banane | droite, gauche | 10 |
| 🍊 Orange | droite, droite | 11 |
"01100011"| Bit lu | Direction | Nœud atteint | Feuille ? Fruit décodé |
|---|---|---|---|
| 0 | gauche | nœud interne gauche | non |
| 1 | droite | Poire | oui → Poire ; on repart de la racine |
| 1 | droite | nœud interne droit | non |
| 0 | gauche | Banane | oui → Banane ; on repart de la racine |
| 0 | gauche | nœud interne gauche | non |
| 0 | gauche | Pomme | oui → Pomme ; on repart de la racine |
| 1 | droite | nœud interne droit | non |
| 1 | droite | Orange | oui → Orange ; fin du message |
Le message décodé est donc : Poire, Banane, Pomme, Orange.
def decoder(arbre, code):
"""Décode une chaîne de 0 et de 1 en suivant l'arbre, et renvoie
la liste des feuilles rencontrées (dans l'ordre)."""
resultat = []
noeud = arbre
for bit in code:
noeud = noeud.gauche if bit == '0' else noeud.droit
if noeud.gauche is None and noeud.droit is None:
# noeud est une feuille : on a fini un symbole
resultat.append(noeud.racine)
noeud = arbre
return resultat
| Opération | Complexité | Pourquoi |
|---|---|---|
hauteur(A) | O(n) | l'appel récursif visite chacun des n nœuds de l'arbre exactement une fois |
taille(A) | O(n) | même raison : chaque nœud est visité une seule fois |
decoder(arbre, code) | O(longueur du code) | chaque bit du code fait descendre le parcours d'un seul niveau dans l'arbre |
creer_arbre_racine(v), racine(A), est_vide(A) | O(1) | ce sont de simples accès à un attribut de l'objet Noeud, sans parcourir l'arbre |
n désigne ici le nombre de nœuds de l'arbre (sa taille). Un arbre binaire de n nœuds a une hauteur minimale égale au nombre de chiffres de n écrit en base 2 (par exemple, un arbre de 197 nœuds a une hauteur minimale de 8, car 197 s'écrit avec 8 chiffres en base deux) ; sa hauteur peut cependant être bien plus grande si l'arbre n'est pas équilibré.
None.Noeud a trois attributs : gauche, racine, droit.