Bonjour,
Dans notre leçon précédente, nous avons analysé la complexité des opérations sur un Arbre Binaire de Recherche (ABR). Nous avons établi que la recherche, l'insertion et la suppression ont toutes une complexité temporelle en , où est la hauteur de l'arbre. Nous avons vu que cette complexité peut varier de dans le meilleur cas (arbre équilibré) à dans le pire cas (arbre dégénéré).
Aujourd'hui, nous allons nous concentrer sur l'implémentation de la plus complexe de ces opérations : la suppression. L'objectif est d'implémenter l'opération de suppression dans un ABR en gérant les trois cas possibles : un nœud sans enfant, avec un enfant, ou avec deux enfants. Maîtriser cet algorithme est fondamental. Il ne s'agit pas seulement d'un exercice de programmation, mais aussi d'une démonstration de rigueur dans la manipulation de structures de données récursives, une compétence clé pour l'Agrégation.
1. Vue d'ensemble des cas de suppression
L'algorithme de suppression commence toujours par la recherche du nœud à supprimer, une opération en . Une fois le nœud trouvé, la procédure de suppression effective dépend du nombre de ses enfants. Il existe trois scénarios distincts.
Pour une introduction claire et en français de ces trois cas, je vous propose de visionner la vidéo suivante.
#10 Les arbres binaires de recherche - La Suppression
Cette vidéo de Programming With HAIRIBI présente de manière très didactique les trois cas de figure que l'on rencontre lors de la suppression d'un nœud dans un ABR.
Regardez l'introduction de 00:00 à 02:18. L'auteur y expose clairement les trois possibilités : le nœud à supprimer est une feuille, il a un seul fils, ou il a deux fils. Nous allons détailler chaque cas.
Analysons maintenant chaque cas en détail.
2. Gestion des cas de suppression
Cas 1 : Le nœud à supprimer est une feuille (0 enfant)
C'est le cas le plus simple. Si le nœud à supprimer n'a aucun enfant, il suffit de le retirer de l'arbre en modifiant le pointeur de son parent (fils gauche ou droit) pour qu'il pointe vers null (ou None en Python).
Illustration de la suppression du nœud 45 (une feuille). Le pointeur left de son parent (60) est simplement mis à null.
L'algorithme consiste donc à :
- Trouver le nœud à supprimer.
- Trouver son parent.
- Mettre à
nullle pointeur du parent qui pointait vers le nœud.
Cas 2 : Le nœud à supprimer a un seul enfant
Ce cas est également assez direct. Pour maintenir la connectivité de l'arbre, on "court-circuite" le nœud à supprimer : on attache son unique sous-arbre directement à son parent. Le parent du nœud supprimé pointe désormais vers l'enfant du nœud supprimé.

La procédure est la suivante :
- Trouver le nœud
Zà supprimer. - Identifier son unique enfant
C. - Modifier le pointeur du parent de
Zpour qu'il pointe versC.
Cette opération préserve la propriété de l'ABR, car tous les éléments dans le sous-arbre de C respectent déjà la relation d'ordre par rapport au parent de Z.
Cas 3 : Le nœud à supprimer a deux enfants
C'est le cas le plus complexe, car nous ne pouvons pas simplement supprimer le nœud sans laisser un "trou" avec deux sous-arbres orphelins. La solution est de trouver un nœud de remplacement qui peut prendre la place du nœud supprimé tout en préservant l'invariant de l'ABR.
Deux candidats sont possibles pour ce remplacement :
- Le successeur en ordre infixe : le plus petit nœud dans le sous-arbre droit.
- Le prédécesseur en ordre infixe : le plus grand nœud dans le sous-arbre gauche.
Par convention, nous utiliserons le successeur.
L'algorithme est le suivant :
- Soit
Zle nœud à supprimer. Trouver son successeur,S(le nœud avec la plus petite clé dans le sous-arbre droit deZ). - Copier la valeur de
SdansZ. À ce stade, l'arbre contient une valeur en double, mais la structure de l'ABR est (temporairement) valide au niveau deZ. - Supprimer récursivement le nœud
Sde sa position d'origine dans le sous-arbre droit.
Le point clé est que la suppression récursive de S tombera nécessairement dans le cas 1 ou le cas 2. En effet, S, étant le plus petit élément de son sous-arbre, ne peut pas avoir de fils gauche. Il est donc soit une feuille, soit il a un seul fils (droit).

Pour bien comprendre la subtilité de cette approche récursive, je vous recommande de regarder l'explication suivante, qui utilise Python.
Delete Node in a BST - Leetcode 450 - Python
La chaîne NeetCodeIO explique avec une grande clarté la logique derrière la suppression d'un nœud à deux enfants, en insistant sur l'étape récursive qui est souvent la plus difficile à appréhender.
Regardez la section de 05:23 à 08:51. L'auteur y explique pourquoi on choisit le minimum du sous-arbre droit (le successeur), et comment l'algorithme se ramène à une suppression récursive d'un cas plus simple. C'est le cœur de l'algorithme.
3. Implémentation
L'implémentation est l'objectif principal de cette leçon. Nous allons voir deux versions : une en Python, que vous allez implémenter, et une en OCaml, pour vous préparer aux exigences de l'Agrégation.
Implémentation en Python
La vidéo de NeetCodeIO que vous venez de consulter pour la partie conceptuelle propose également une implémentation complète en Python. Votre tâche consiste maintenant à l'écrire vous-même.
Delete Node in a BST - Leetcode 450 - Python
Cette section de la vidéo de NeetCodeIO montre comment traduire la logique de suppression en code Python récursif.
Regardez la section de 08:51 à 12:21. Portez une attention particulière à la structure de la fonction récursive : les premiers if/elif gèrent la recherche du nœud, et le else final contient la logique de suppression pour les trois cas. Notez comment les cas 0 et 1 enfant sont gérés élégamment en retournant root.right ou root.left.
Exercice d'implémentation :
En vous basant sur la logique expliquée, implémentez la fonction delete(self, value) pour la classe BinarySearchTree ci-dessous.
class TreeNode:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
class BinarySearchTree:
def __init__(self):
self.root = None
# ... (les fonctions d'insertion et de recherche que vous avez déjà écrites)
def _find_min(self, node):
current = node
while current and current.left is not None:
current = current.left
return current
def delete(self, key):
self.root = self._delete_recursive(self.root, key)
def _delete_recursive(self, node, key):
# Cas de base: l'arbre est vide
if not node:
return node
# Recherche du noeud à supprimer
if key < node.key:
node.left = self._delete_recursive(node.left, key)
elif key > node.key:
node.right = self._delete_recursive(node.right, key)
else:
# Le noeud à supprimer est trouvé
# Cas 1: 0 ou 1 enfant
if not node.left:
return node.right
elif not node.right:
return node.left
# Cas 2: 2 enfants
# Trouver le successeur (le plus petit dans le sous-arbre droit)
temp = self._find_min(node.right)
# Copier la valeur du successeur dans ce noeud
node.key = temp.key
# Supprimer le successeur de sa position d'origine
node.right = self._delete_recursive(node.right, temp.key)
return node
Suggestion de squelette pour votre implémentation. Je vous encourage à l'écrire de zéro pour bien vous l'approprier, mais vous pouvez vous référer à ce code complet si besoin.
Formalisation en OCaml
Pour l'Agrégation, une maîtrise des concepts sous un angle fonctionnel est un atout majeur. Le document suivant, destiné à des classes préparatoires de haut niveau, présente une implémentation de la suppression en OCaml.
[PDF] Arbres binaires - info-llg.fr
Ce document propose une implémentation élégante et formelle de la suppression en OCaml. C'est un excellent exemple du niveau de rigueur attendu.
Lisez la section 2.3 "Insertion et suppression". Concentrez-vous sur l'algorithme de suppression et le code OCaml associé. Identifiez la fonction supprime et la fonction auxiliaire supprime_min. Analysez comment le pattern matching d'OCaml permet de gérer les différents cas de manière concise.
L'implémentation OCaml supprime k utilise le pattern matching pour distinguer les cas :
Noeud (Nil, x, fd) -> fd: Le nœudxn'a pas de fils gauche. On renvoie son fils droitfd. (Cas 1 et 2)Noeud (fg, x, Nil) -> fg: Le nœudxn'a pas de fils droit. On renvoie son fils gauchefg. (Cas 2)Noeud (fg, x, fd) -> ...: Le nœud a deux fils. On appellesupprime_min fdpour trouver le successeurmet le nouvel arbre droitf, puis on reconstruit le nœud :Noeud (fg, m, f).
Cette approche est purement fonctionnelle : elle ne modifie pas l'arbre en place mais retourne un nouvel arbre.
Conclusion
Aujourd'hui, nous avons décortiqué et implémenté l'algorithme de suppression dans un ABR, l'opération la plus délicate sur cette structure.
Points clés à retenir :
- La suppression se divise en trois cas selon le nombre d'enfants du nœud à supprimer (0, 1 ou 2).
- Les cas avec 0 ou 1 enfant sont simples et consistent en un réarrangement de pointeurs.
- Le cas avec 2 enfants est le plus complexe. Il est résolu en remplaçant la valeur du nœud par celle de son successeur (ou prédécesseur) et en supprimant récursivement ce dernier de sa position initiale.
- Cette suppression récursive se ramène nécessairement à un cas plus simple (0 ou 1 enfant).
- L'ensemble de l'opération (recherche + réarrangement) a une complexité en , confirmant l'analyse de la leçon précédente.
Prochaine étape :
Nous avons maintenant une structure de données complète avec insertion, recherche et suppression efficaces... en théorie. Nous avons vu que la performance dépend crucialement de la hauteur de l'arbre. Les opérations d'insertion et de suppression que nous avons écrites ne garantissent en rien que l'arbre restera équilibré. Au contraire, une suite malheureuse d'opérations peut le rendre complètement dégénéré. Dans la prochaine leçon, nous aborderons de front ce problème du déséquilibre et introduirons le principe des arbres auto-équilibrants (comme les arbres AVL), qui sont la solution canonique à cette limitation des ABR simples.