Bonjour,
Dans notre leçon précédente, nous avons exploré les algorithmes de parcours qui permettent de visiter chaque nœud d'un arbre binaire. Nous avions terminé sur une note suggestive : pour un certain type d'arbre, le parcours infixe produit une séquence de valeurs triées. Aujourd'hui, nous allons étudier cette structure remarquable.
L'objectif de cette leçon est d'implémenter l'Arbre Binaire de Recherche (ABR), ou Binary Search Tree (BST) en anglais. C'est une structure de données fondamentale, au cœur de nombreuses applications nécessitant des opérations de recherche, d'ajout et de suppression efficaces. Nous allons :
- Définir formellement la propriété d'un ABR.
- Concevoir et implémenter l'algorithme de recherche.
- Concevoir et implémenter l'algorithme d'insertion.
Cette leçon vous permettra de passer de la manipulation générale des arbres à l'exploitation d'une structure ordonnée pour gagner en efficacité, un principe central en algorithmique.
1. Définition de l'Arbre Binaire de Recherche (ABR)
Un ABR est un arbre binaire qui satisfait une contrainte d'ordre stricte, appelée "propriété de l'arbre binaire de recherche".
Pour une étude formelle de cette définition, je vous invite à lire la section suivante.
Arbres binaires de recherche - Définition
Cette ressource de 'l'Informatique, c'est fantastique !' pose la définition formelle d'un ABR. Elle est claire et précise.
Lisez la section "Définition" au début de la page. Concentrez-vous sur la contrainte d'ordre qui lie un nœud à ses sous-arbres gauche et droit.
En résumé, pour n'importe quel nœud n de l'arbre possédant une clé k :
- Toutes les clés dans le sous-arbre gauche de
ndoivent être inférieures àk. - Toutes les clés dans le sous-arbre droit de
ndoivent être supérieures àk. - Les deux sous-arbres, gauche et droit, doivent également être des arbres binaires de recherche.
- Par convention, nous interdirons les clés dupliquées.
Cette propriété est la clé de toute l'efficacité de la structure.
2. La recherche dans un ABR
Comment la propriété de l'ABR nous aide-t-elle à trouver une valeur v ? L'algorithme est une application directe du principe "diviser pour régner" :
- On commence à la racine de l'arbre.
- On compare
vavec la clé du nœud courant,k:- Si
v == k, la valeur est trouvée. - Si
v < k, la recherche continue récursivement dans le sous-arbre gauche. - Si
v > k, la recherche continue récursivement dans le sous-arbre droit.
- Si
- Si on atteint un sous-arbre vide (
None), cela signifie quevn'est pas dans l'arbre.
La vidéo suivante illustre ce processus et analyse sa complexité.
Binary Tree Part 1 | BST | Binary Search Tree - Data Structures & Algorithms Tutorials In Python #10
Cette vidéo de la chaîne codebasics explique clairement le principe de la recherche dans un ABR et pourquoi elle est efficace.
Regardez la section de 03:13 à 05:30. Portez une attention particulière à l'explication de la complexité temporelle en O(log n). Comprendre pourquoi on élimine à chaque étape une grande partie de l'arbre est essentiel.
Comme vous l'avez vu, la complexité de la recherche est proportionnelle à la hauteur h de l'arbre, soit . Dans un arbre raisonnablement équilibré, la hauteur est de l'ordre de , où n est le nombre de nœuds. La recherche est donc très rapide. Cependant, dans un cas dégénéré (un arbre filiforme), la hauteur peut être n, et la complexité redevient linéaire, .
Implémentation en Python
Voici à quoi ressemble une implémentation récursive de la recherche. Elle est une traduction directe de l'algorithme.
class Noeud:
def __init__(self, cle):
self.cle = cle
self.gauche = None
self.droit = None
def rechercher(racine, cle_recherchee):
# Cas de base : l'arbre est vide ou la clé est trouvée
if racine is None or racine.cle == cle_recherchee:
return racine
# La clé recherchée est plus petite, on cherche à gauche
if cle_recherchee < racine.cle:
return rechercher(racine.gauche, cle_recherchee)
# La clé recherchée est plus grande, on cherche à droite
else: # cle_recherchee > racine.cle
return rechercher(racine.droit, cle_recherchee)
Cette fonction retourne le Noeud s'il est trouvé, et None sinon.
3. L'insertion dans un ABR
L'insertion d'une nouvelle clé v doit préserver la propriété de l'ABR. L'algorithme est très similaire à celui de la recherche :
- On cherche la position où
vdevrait se trouver si elle existait dans l'arbre. - Cette recherche nous mènera inévitablement à un lien
None(un sous-arbre vide). - On crée un nouveau nœud avec la clé
vet on l'attache à ce lien.
L'image suivante illustre ce processus pour l'insertion de la valeur 12.

Implémentation récursive en Python
L'implémentation récursive de l'insertion est élégante, mais comporte une subtilité importante concernant la mise à jour des pointeurs. La vidéo suivante l'explique remarquablement bien.
Insert into a Binary Search Tree - Leetcode 701 - Python
La chaîne NeetCodeIO est une référence pour la préparation aux entretiens techniques. Cette vidéo explique la logique de l'insertion récursive de manière très pédagogique.
Regardez les sections de 03:27 à 04:38 et de 05:57 à 07:35. Le point crucial à comprendre est la ligne root.left = insert(root.left, val). Elle assure que le lien parental est correctement mis à jour avec le nouvel arbre (potentiellement modifié par l'insertion).
En nous basant sur cette explication, voici une fonction d'insertion propre.
def inserer(racine, cle_a_inserer):
# Cas de base : si l'arbre est vide, on retourne un nouveau noeud
if racine is None:
return Noeud(cle_a_inserer)
# Si la clé existe déjà, on ne fait rien
if cle_a_inserer == racine.cle:
return racine
# La clé à insérer est plus petite, on insère dans le sous-arbre gauche
if cle_a_inserer < racine.cle:
racine.gauche = inserer(racine.gauche, cle_a_inserer)
# La clé à insérer est plus grande, on insère dans le sous-arbre droit
else: # cle_a_inserer > racine.cle
racine.droit = inserer(racine.droit, cle_a_inserer)
return racine
Notez que la fonction inserer retourne toujours la racine du sous-arbre (modifié ou non). C'est cette valeur de retour qui permet au parent de "raccrocher" le sous-arbre mis à jour.
4. Perspective fonctionnelle en OCaml
Votre préparation à l'Agrégation exige une maîtrise de la programmation fonctionnelle. L'implémentation d'un ABR en OCaml est un excellent exercice qui met en lumière des concepts clés comme l'immutabilité et le pattern matching.
Contrairement à l'approche impérative en Python où nous modifions l'arbre en place, l'approche fonctionnelle consiste à créer un nouvel arbre qui inclut la valeur insérée, tout en réutilisant les parties non modifiées de l'arbre original pour des raisons d'efficacité.
Principles of Programming Languages - Trees
Ces notes de cours de l'Université Johns Hopkins présentent la définition d'un type d'arbre et l'implémentation des fonctions lookup (recherche) et insert en OCaml.
Dans la section "OCaml Lecture V", lisez la partie "Trees". Observez la définition du type 'a btree. Étudiez la fonction lookup. Notez sa similarité structurelle avec la version récursive en Python. Étudiez attentivement la fonction insert. Comprenez bien qu'elle ne modifie pas l'arbre bst passé en argument, mais retourne un nouvel arbre.
La fonction insert en OCaml illustre un principe fondamental :let rec insert x bst = match bst with ...
Cette fonction ne dit pas "modifie bst", mais "laisse insert x bst être un nouvel arbre construit à partir de bst et x". Cette distinction est au cœur du paradigme fonctionnel.
5. Exercice d'application
Pour consolider votre compréhension, mettons en pratique ces algorithmes.
- Créez une classe
ABRen Python qui encapsulera la racine de votre arbre. - Implémentez les méthodes
inserer(self, cle)etrechercher(self, cle)au sein de cette classe. Ces méthodes appelleront les fonctions récursives que nous avons définies, en utilisant la racine de l'arbre (self.racine). - Instanciez un ABR vide.
- Insérez-y les clés suivantes, dans cet ordre :
[50, 30, 70, 20, 40, 60, 80]. - Dessinez l'arbre final sur papier.
- Vérifiez avec votre méthode
rechercherque la clé40est présente et que la clé90est absente.
Cliquez ici pour voir la structure de la classe et la forme de l'arbre final
Structure de la classe :
class ABR:
def __init__(self):
self.racine = None
def inserer(self, cle):
self.racine = self._inserer_recursif(self.racine, cle)
def _inserer_recursif(self, racine_actuelle, cle_a_inserer):
# Reprendre le code de la fonction 'inserer' vue plus haut
if racine_actuelle is None:
return Noeud(cle_a_inserer)
if cle_a_inserer < racine_actuelle.cle:
racine_actuelle.gauche = self._inserer_recursif(racine_actuelle.gauche, cle_a_inserer)
elif cle_a_inserer > racine_actuelle.cle:
racine_actuelle.droit = self._inserer_recursif(racine_actuelle.droit, cle_a_inserer)
return racine_actuelle
def rechercher(self, cle):
return self._rechercher_recursif(self.racine, cle) is not None
def _rechercher_recursif(self, racine_actuelle, cle_recherchee):
# Reprendre le code de la fonction 'rechercher' vue plus haut
if racine_actuelle is None or racine_actuelle.cle == cle_recherchee:
return racine_actuelle
if cle_recherchee < racine_actuelle.cle:
return self._rechercher_recursif(racine_actuelle.gauche, cle_recherchee)
else:
return self._rechercher_recursif(racine_actuelle.droit, cle_recherchee)
Arbre résultant :
50
/ \
30 70
/ \ / \
20 40 60 80
C'est un arbre binaire parfaitement équilibré. Appliquer un parcours infixe sur cet arbre vous donnerait la séquence 20, 30, 40, 50, 60, 70, 80.
Conclusion
Aujourd'hui, nous avons fait un pas décisif en ajoutant une contrainte d'ordre à la structure d'arbre binaire pour créer l'ABR.
Points clés à retenir :
- La propriété de l'ABR (gauche < racine < droit) est la règle qui gouverne toute la structure.
- Les opérations de recherche et d'insertion suivent un chemin logique le long de l'arbre, exploitant cette propriété pour une complexité moyenne en .
- La performance de l'ABR dépend crucialement de sa hauteur. Un arbre déséquilibré peut entraîner des performances linéaires en , ce qui annule son avantage.
- L'implémentation peut être impérative (modification en place, comme en Python) ou fonctionnelle (création de nouvelles structures, comme en OCaml), deux paradigmes que vous devez maîtriser.
Prochaine étape :
Nous avons entrevu que l'efficacité de l'ABR est liée à son "équilibre". Mais que se passe-t-il si on insère les clés dans l'ordre [20, 30, 40, 50, 60, 70, 80] ? L'arbre devient une longue chaîne. Dans la prochaine leçon, nous analyserons formellement la complexité des opérations sur un ABR dans le meilleur et le pire des cas, ce qui nous motivera à étudier, plus tard, les arbres auto-équilibrants.