Un arbre binaire est une structure de données abstraite formée d’un ensemble de nœuds organisés hiérarchiquement selon la définition inductive suivante :
Un arbre binaire est :
soit un arbre vide, noté $E$, ne contenant aucun nœud ;
soit un nœud, appelé racine, relié à exactement deux arbres binaires $g$ et $d$, respectivement appelés sous-arbres gauche et sous-arbre droit.
On note $T(r,g,d)$ l’arbre non vide dont la racine $r$ (on peut aussi indiquer l’étiquette de cette racine).
Vocabulaire et remarques
Un nœud dont les deux sous-arbres sont vides est appelé une feuille.
Un nœud qui n’est pas une feuille est un nœud interne.
Un arbre binaire est dit parfait si toutes les feuilles sont à la même profondeur .
La racine d’un arbre $T$ est le seul nœud de $T$ sans parent.
Propriétés
Position des sous-arbres
La position des sous-arbres gauche et droite est fondamentale : $$T(r,g,d) \neq T(r,d,g)$$
Taille d’un arbre
La taille d’un arbre est égale au nombre de nœuds qu’il comporte.
Le nombre de nœuds d’un arbre binaire $T$, noté $n(T)$, se calcule récursivement :
$n(E)=0$
$n(T)=1+n(g)+n(d)$
Profondeur d’un nœud
La profondeur d’un nœud $x$, notée $h(x)$, est sa distance à la racine.
La profondeur d’un nœud $x$, notée $h(x)$, est définie récursivement par :
$h(\text{racine})=0$
$h(x)=1+h(\text{parent de }x)$
La profondeur d’un nœud dans un arbre est donc le nombre d’arrêtes qu’il faut parcourir, depuis la racine, pour parvenir au nœud.
La définition de la profondeur d’un nœud peut varier !
Hauteur d’un arbre
La hauteur d’un arbre est la profondeur la plus grande atteinte par ses nœuds.
La hauteur d’un arbre binaire $T$, notée $h(T)$ est définie récursivement par :
$h(E)=-1$
$h(T)=1+\mathrm{max}(h(g), h(d))$
Remarque
On peut aussi définir la hauteur d’un nœud : la hauteur d’un nœud est la distance entre ce nœud et la feuille la plus profonde ayant une relation de descendance avec ce nœud.
La définition de la hauteur d’un arbre peut varier !
Relation entre le nombre de nœuds $n$ et la hauteur d’un arbre
Soit $T$ un arbre binaire, $n$ son nombre de nœuds et $h$ sa hauteur. Ces grandeurs sont liées par les relations suivantes :
$h + 1 \leq n \leq 2^{h+1} - 1$
le nombre de sous-arbres vides de $T$ est $n+1$.
Dans quel cas a-t-on $N = h+1$ ?
Réponse
On a $N = h+1$ lorsque chaque nœud ne possède qu’un seul nœud fils. Il s’agit donc d’une structure linéaire (liste).
Dans quel cas a-t-on $N = 2^{h+1} - 1$ ?
Réponse
On a $N = 2^{h+1} - 1$ lorsque l’arbre binaire est parfait, c’est à dire lorsque toutes les feuilles ont la même profondeur.
Spécification des arbres binaires
Créer un nœud (type) :
fonction : creer_nœud() $\longrightarrow$ retourne un nœud ;
fils_gauche et fils_droit sont automatiquement initialisés à None ;;
complexité : $O(0)$ (pas tout à fait vrai mais suffisant pour comprendre la suite).
Lire dans le champ valeur :
fonction : lire_valeur(n: Noeud[T]) $\longrightarrow$ T ;
complexité : 1 opération élémentaire.
Écrire dans le champ valeur :
fonction : modifier_valeur(n: Noeud[T], valeur: T) $\longrightarrow$ None ;
complexité : 1 opération élémentaire.
Lire dans le champ fils_gauche :
fonction : lire_filsG(n: Noeud[T]) $\longrightarrow$ Noeud[T] ;
complexité : 1 opération élémentaire.
Écrire dans le champ fils_gauche :
fonction : modifier_filsG(n: Noeud[T], m: Noeud[T]) $\longrightarrow$ None ;
complexité : 1 opération élémentaire.
Lire dans le champ fils_droit :
fonction : lire_filsD(n: Noeud[T]) $\longrightarrow$ Noeud[T] ;
complexité : 1 opération élémentaire.
Écrire dans le champ fils_droit :
fonction : modifier_filsD(n: Noeud[T], m: Noeud[T]) $\longrightarrow$ None ;
complexité : 1 opération élémentaire.
Tout comme on a désigné les listes chaînées par leur première cellule, on va désigner les arbres par leur nœud racine.
Implémentation de la spécification en Python
Il existe de nombreuses façons de d’implémenter la structure d’arbre en Python. Dans cette partie on va utiliser une classe.
Le paradigme de programmation utilisé ci-dessous est le paradigme impératif : on crée dans un premier temps une structure élémentaire à l’aide d’une classe et on définit ensuite des fonctions qui manipulent cette structure.
On ne crée pas de structure arbre binaire (accompagnée de méthodes).
Écrire le code de la classe Noeud respectant la spécification. Les trois attributs seront nommés valeur, gauche, droit.
Écrire le code permettant de construire l’arbre :
graph TD
A("A") --- B("B")
B --- G(None)
B --- C("C")
C --- H(None)
C --- I(None)
A --- D("D")
D --- E(None)
D --- F(None)
from__future__importannotationsclassNoeud():"""
Implémentation d'un noeud.
"""def__init__(self:Noeud,valeur:str,gauche:Noeud=None,droit:Noeud=None)->None:self.valeur=valeurself.gauche=gaucheself.droit=droitdefest_vide(n:Noeud)->bool:"""
Teste si un arbre est vide.
"""returnnisNonedefajoute_filsG(n:Noeud,valeur:str)->None:"""
Ajoute un noeud d'étiquette valeur comme fils gauche.
"""ifest_vide(n.gauche):new_n=Noeud(valeur)n.gauche=new_nelse:raiseException("Noeud possède déjà un fils à gauche.")defajoute_filsD(n:Noeud,valeur:str)->None:"""
Ajoute un noeud d'étiquette valeur comme fils droit.
"""ifest_vide(n.droit):new_n=Noeud(valeur)n.droit=new_nelse:raiseException("Noeud possède déjà un fils à droite.")defest_feuille(n:Noeud)->bool:"""
Teste si un noeud est une feuille.
"""returnest_vide(n.gauche)andest_vide(n.droit)defdegre_noeud(n:Noeud)->int:"""
Détermine le degré du noeud passé en argument.
"""nbre=0ifnotest_vide(nbre.gauche):nbre+=1ifnotest_vide(nbre.droit):nbre+=1returnnbreif__name__=="__main__":n1=Noeud('A')ajoute_filsG(n1,'B')ajoute_filsD(n1.gauche,'C')ajoute_filsD(n1,'D')print("Pour n1, D est-il une feuille : {}".format(est_feuille(n1.droit)))print("Pour n1, B est-il une feuille : {}".format(est_feuille(n1.gauche)))print("Pour n1, C est-il une feuille : {}".format(est_feuille(n1.gauche.droit)))print("Degré de la racine : {}".format(degre_noeud(n1)))print("Degré de B : {}".format(degre_noeud(n1.gauche)))print("Degré de D : {}".format(degre_noeud(n1.droit)))print("Degré de C : {}".format(degre_noeud(n1.gauche.droit)))
Quelques algorithmes de manipulation des arbres binaires
Algorithmes récursifs
Écrire la spécification et le code de la fonction taille dont la spécification est :
1
2
3
4
deftaille(n:Noeud)->int:"""
Retourne le nombre de noeud dans l'arbre.
"""
Réponse
1
2
3
4
5
6
7
8
deftaille(n:Noeud)->int:"""
Retourne le nombre de noeud dans l'arbre depuis le noeud n
"""ifest_vide(n):return0else:return1+taille(n.gauche)+taille(n.droit)
Écrire la spécification et le code de la fonction profondeur dont la spécification est :
1
2
3
4
defprofondeur(n:Noeud)->int:"""
Retourne la profondeur de l'arbre.
"""
Réponse
1
2
3
4
5
6
7
8
defprofondeur(n:Noeud)->int:"""
Retourne la profondeur de l'arbre
"""ifest_vide(n):return0else:return1+max(profondeur(n.gauche),profondeur(n.droit))
Écrire la spécification et le code de la fonction nbre_feuilles dont la spécification est :
1
2
3
4
defnbre_feuilles(n:Noeud)->int:"""
Détermine le nombre de feuilles dans l'arbre.
"""
Réponse
1
2
3
4
5
6
7
8
9
10
defnbre_feuilles(n:Noeud)->int:"""
Détermine le nombre de feuilles dans l'arbre.
"""ifest_vide(n):# Cas des nœuds de degré 1 (ou de l'arbre vide)return0elifest_feuille(n):return1else:return0+nbre_feuilles(n.gauche)+nbre_feuilles(n.droit)
Quelles sont les complexités des algorithmes implémentés dans cette section ?
Réponse
Les algorithmes parcourent une fois chaque nœud de l’arbre. La complexité est donc proportionnelle au nombre de nœuds.
Parcours en profondeur d’un arbre
Les fonction écrites jusqu’à présent parcourent en profondeur tous les nœuds des arbres sans que l’ordre dans lequel ce parcourt est effectué ait la moindre importance.
Si on souhaite donner un affichage de l’arbre, la façon dont on le parcourt prend alors une grande importance. On peut :
Parcourir le sous-arbre gauche, afficher la valeur du nœud puis enfin parcourir le sous-arbre droit. On parle d’un parcours infixe.
Afficher la valeur de chaque nœud avant de parcourir les sous-arbres. On parle d’un parcours préfixe.
Parcourir les sous-arbres puis afficher les valeurs. On parle d’un parcours suffixe.
Pour l’arbre ci-dessous, donner les parcours préfixe, infixe et suffixe.
Écrire les spécifications et le code des fonctions affiche_infixe, affiche_prefixe et affiche_suffixe dont les spécifications sont :
1
2
3
4
defaffiche_infixe(n:Noeud)->None:"""
Parcourt le sous-arbre gauche, affiche la valeur du nœud puis parcourt le sous-arbre droit.
"""
1
2
3
4
defaffiche_prefixe(n:Noeud)->None:"""
Parcourt le sous-arbre gauche, affiche la valeur du nœud puis parcourt le sous-arbre droit.
"""
1
2
3
4
defaffiche_suffixe(n:Noeud)->None:"""
Parcourt le sous-arbre gauche, affiche la valeur du nœud puis parcourt le sous-arbre droit.
"""
Algorithmes itératifs
Reprendre les questions précédentes en utilisant des algorithmes itératifs.
Parcours d’un arbre à l’aide d’une file ou d’une pile
Parcours en largeur d’un arbre
Décrire le fonctionnement de l’algorithme suivant et montrer qu’il réalise un parcours en largeur d’un arbre.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Fonction parcours_largeur(racine: Noeud):
File f= newFile() f.enfiler(racine) TantQue non f.est_vide():
n= f.defiler() print(n.valeur) Si non est_vide(n.filsG):
f.enfiler(n.filsG) FinSi
Si non est_vide(n.filsD):
f.enfiler(n.filsD) FinSi
FinTantQue
FinFonction
Implémenter en Python l’algorithme précédent.
Parcours en profondeur d’un arbre
Décrire le fonctionnement de l’algorithme suivant et montrer qu’il réalise un parcours en profondeur d’un arbre.
De quel type de parcours en profondeur s’agit-il ?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Fonction parcours_profondeur(racine: Noeud):
Pile p= newPile() p.empiler(racine) TantQue non est_vide(p):
n= p.depiler() print(n) Si non est_vide(n.filsG):
empiler(n.filsG) FinSi
Si non est_vide(n.filsD):
empiler(n.filsD) FinSi
FinTantQue
FinFonction
Implémenter en Python l’algorithme précédent.
À retenir
Un arbre binaire est un ensemble fini de nœuds, qui est soit vide, soit structuré à partir d’un nœud particulier, appelé racine de l’arbre, et de deux sous-ensembles de nœuds formant récursivement un sous-arbre gauche et un sous-arbre droit.
Un arbre peut-être implémenté en Python avec un objet par nœud, d’une classe qui possède trois attributs : la valeur (ou étiquette) du nœud, le sous-arbre gauche et le sous-arbre droit. La valeur None est utilisée pour représenter l’arbre vide.