Aller au contenu

Chapitre 5 - Partie 3 : Arbres binaires de recherche et parcours


Un arbre binaire est soit :

  • l'arbre vide (noté None en Python) ;
  • un nœud contenant une valeur (appelée étiquette ou clé), un sous-arbre gauche et un sous-arbre droit, qui sont eux-mêmes des arbres binaires.
8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 7 13
TermeDéfinition
RacineNœud au sommet de l'arbre (ici : 8)
FeuilleNœud sans enfants (ici : 1, 4, 7, 13)
Nœud interneNœud ayant au moins un enfant
Sous-arbre gauche/droitArbre formé par l'enfant gauche/droit et ses descendants
TailleNombre total de nœuds
HauteurLongueur du plus long chemin de la racine à une feuille
Profondeur d'un nœudNombre d'arêtes entre la racine et ce nœud
class Noeud:
def __init__(self, valeur, gauche=None, droit=None):
self.valeur = valeur
self.gauche = gauche
self.droit = droit

Construction de l'arbre de l'exemple :

arbre = Noeud(8,
Noeud(3,
Noeud(1),
Noeud(6, Noeud(4), Noeud(7))
),
Noeud(10,
None,
Noeud(14, Noeud(13), None)
)
)

La taille est définie récursivement :

taille(a)={0si a est vide1+taille(gauche)+taille(droit)sinon\text{taille}(a) = \begin{cases} 0 & \text{si } a \text{ est vide} \\ 1 + \text{taille}(\text{gauche}) + \text{taille}(\text{droit}) & \text{sinon} \end{cases}

def taille(arbre):
if arbre is None:
return 0
return 1 + taille(arbre.gauche) + taille(arbre.droit)
>>> taille(arbre)
9

La hauteur est la profondeur maximale. Par convention, l'arbre vide a une hauteur de -1 (ou 0 selon les conventions).

hauteur(a)={−1si a est vide1+max⁡(hauteur(gauche),hauteur(droit))sinon\text{hauteur}(a) = \begin{cases} -1 & \text{si } a \text{ est vide} \\ 1 + \max(\text{hauteur}(\text{gauche}), \text{hauteur}(\text{droit})) & \text{sinon} \end{cases}

def hauteur(arbre):
if arbre is None:
return -1
return 1 + max(hauteur(arbre.gauche), hauteur(arbre.droit))
>>> hauteur(arbre)
3

Pour un arbre binaire de taille n :

⌊log⁡2(n)⌋≤h≤n−1\lfloor \log_2(n) \rfloor \leq h \leq n - 1

  • Hauteur minimale (arbre complet/équilibré) : h=⌊log⁡2(n)⌋h = \lfloor \log_2(n) \rfloor
  • Hauteur maximale (arbre filiforme, dégénéré en liste) : h=n−1h = n - 1
def nb_feuilles(arbre):
if arbre is None:
return 0
if arbre.gauche is None and arbre.droit is None:
return 1
return nb_feuilles(arbre.gauche) + nb_feuilles(arbre.droit)

Les parcours en profondeur (Depth-First Search, DFS) explorent l'arbre en descendant le plus profondément possible avant de remonter. Ils diffèrent par le moment où l'on traite la racine.

On traite la racine d'abord, puis le sous-arbre gauche, puis le sous-arbre droit.

def parcours_prefixe(arbre):
if arbre is None:
return []
return ([arbre.valeur]
+ parcours_prefixe(arbre.gauche)
+ parcours_prefixe(arbre.droit))

Sur notre exemple : 8, 3, 1, 6, 4, 7, 10, 14, 13

On traite d'abord le sous-arbre gauche, puis la racine, puis le sous-arbre droit.

def parcours_infixe(arbre):
if arbre is None:
return []
return (parcours_infixe(arbre.gauche)
+ [arbre.valeur]
+ parcours_infixe(arbre.droit))

Sur notre exemple : 1, 3, 4, 6, 7, 8, 10, 13, 14

On traite les sous-arbres d'abord, puis la racine en dernier.

def parcours_suffixe(arbre):
if arbre is None:
return []
return (parcours_suffixe(arbre.gauche)
+ parcours_suffixe(arbre.droit)
+ [arbre.valeur])

Sur notre exemple : 1, 4, 7, 6, 3, 13, 14, 10, 8

ParcoursOrdreRésultat sur l'exemple
PréfixeRacine → Gauche → Droit8, 3, 1, 6, 4, 7, 10, 14, 13
InfixeGauche → Racine → Droit1, 3, 4, 6, 7, 8, 10, 13, 14
SuffixeGauche → Droit → Racine1, 4, 7, 6, 3, 13, 14, 10, 8

Le parcours en largeur (Breadth-First Search, BFS) explore l'arbre niveau par niveau, de gauche à droite. Il utilise une file (FIFO).

from collections import deque
def parcours_largeur(arbre):
if arbre is None:
return []
resultat = []
file = deque([arbre])
while file:
noeud = file.popleft()
resultat.append(noeud.valeur)
if noeud.gauche is not None:
file.append(noeud.gauche)
if noeud.droit is not None:
file.append(noeud.droit)
return resultat

Sur notre exemple : 8, 3, 10, 1, 6, 14, 4, 7, 13


Un arbre binaire de recherche (ABR) est un arbre binaire vérifiant pour chaque nœud :

  • toutes les valeurs du sous-arbre gauche sont inférieures à la valeur du nœud ;
  • toutes les valeurs du sous-arbre droit sont supérieures à la valeur du nœud.

La recherche se fait par dichotomie : à chaque nœud, on choisit de descendre à gauche ou à droite.

def recherche_abr(arbre, cle):
if arbre is None:
return False
if cle == arbre.valeur:
return True
elif cle < arbre.valeur:
return recherche_abr(arbre.gauche, cle)
else:
return recherche_abr(arbre.droit, cle)

Complexité : dans un ABR équilibré, la recherche est en O(log₂ n) (on divise l'espace de recherche par 2 à chaque étape). Dans le pire cas (arbre dégénéré), elle est en O(n).

Pour insérer une valeur, on descend dans l'arbre comme pour une recherche, puis on crée un nouveau nœud à l'emplacement vide trouvé.

def inserer_abr(arbre, cle):
if arbre is None:
return Noeud(cle)
if cle < arbre.valeur:
arbre.gauche = inserer_abr(arbre.gauche, cle)
elif cle > arbre.valeur:
arbre.droit = inserer_abr(arbre.droit, cle)
return arbre
# Construction d'un ABR par insertions successives
racine = None
for val in [8, 3, 10, 1, 6, 14, 4, 7, 13]:
racine = inserer_abr(racine, val)

Arbre binaire de recherche interactif : activez JavaScript pour utiliser cette visualisation.

ConceptDescription
Arbre binaireStructure récursive : vide ou nœud + sous-arbre gauche + sous-arbre droit
TailleNombre de nœuds (récursif : 1 + taille gauche + taille droit)
HauteurProfondeur maximale (récursif : 1 + max des hauteurs)
Parcours préfixeRacine → Gauche → Droit
Parcours infixeGauche → Racine → Droit (donne l'ordre croissant dans un ABR)
Parcours suffixeGauche → Droit → Racine
Parcours en largeurNiveau par niveau, avec une file
ABRArbre binaire de recherche : gauche < racine < droit
Recherche ABRO(log₂ n) si l'arbre est équilibré