Structures de données — 04 — Les arbres binaires de recherche

L'arbre binaire de recherche (BST) : invariant, insertion, recherche et parcours en ordre. Pourquoi il offre O(log n) en théorie mais peut dégénérer en O(n).

04 — Les arbres binaires de recherche

Ce que tu vas apprendre

  • L'invariant d'un arbre binaire de recherche
  • Insertion, recherche et parcours en ordre
  • Pourquoi les opérations sont en O(hauteur)
  • Le problème de la dégénérescence en O(n)

Prérequis


Les structures vues jusqu'ici sont linéaires. L'arbre binaire de recherche (BST, binary search tree) est la première structure arborescente de la série. Il combine deux qualités qu'aucune structure précédente n'avait ensemble : une recherche rapide, comme la dichotomie, et le maintien des éléments dans l'ordre trié, ce que la hashmap ne fait pas. C'est la base conceptuelle des index de bases de données et des structures ordonnées.

L'invariant

Un BST est un arbre où chaque nœud a au plus deux enfants, gauche et droite, et respecte une règle stricte : pour tout nœud, toutes les valeurs du sous-arbre gauche sont inférieures à sa valeur, et toutes celles du sous-arbre droit sont supérieures. Cet invariant tient à tous les niveaux.

typescriptclass NoeudBST {
  valeur: number;
  gauche: NoeudBST | null = null;
  droite: NoeudBST | null = null;
  constructor(valeur: number) { this.valeur = valeur; }
}

La conséquence directe de l'invariant : chercher une valeur revient à descendre l'arbre en éliminant la moitié à chaque nœud, exactement comme la recherche dichotomique de la série Recherche, mais sur une structure dynamique qu'on peut modifier.

Recherche et insertion

Pour chercher, on compare la cible au nœud courant : si elle est plus petite, on descend à gauche ; plus grande, à droite ; égale, on a trouvé. L'insertion suit le même chemin jusqu'à une place libre.

pythonclass NoeudBST:
    def __init__(self, valeur):
        self.valeur = valeur
        self.gauche = None
        self.droite = None

def rechercher(racine, cible):
    while racine is not None:
        if cible == racine.valeur:
            return True
        racine = racine.gauche if cible < racine.valeur else racine.droite
    return False

def inserer(racine, valeur):
    if racine is None:
        return NoeudBST(valeur)
    if valeur < racine.valeur:
        racine.gauche = inserer(racine.gauche, valeur)
    elif valeur > racine.valeur:
        racine.droite = inserer(racine.droite, valeur)
    return racine  # les doublons sont ignorés ici

Chaque opération descend d'un niveau à chaque étape. Le coût est donc O(hauteur de l'arbre). Si l'arbre est équilibré, la hauteur est log n, et la recherche est O(log n). C'est là toute la promesse du BST.

Le parcours en ordre

L'invariant offre un bonus que la hashmap n'a pas : parcourir les éléments dans l'ordre trié, gratuitement. Il suffit de visiter le sous-arbre gauche, puis le nœud, puis le sous-arbre droit. C'est le parcours infixe (in-order).

pythondef parcours_ordonne(racine, resultat):
    if racine is not None:
        parcours_ordonne(racine.gauche, resultat)
        resultat.append(racine.valeur)        # nœud entre gauche et droite
        parcours_ordonne(racine.droite, resultat)
    return resultat

Ce parcours visite tous les nœuds une fois, en O(n), et les sort triés. Le BST permet aussi de trouver efficacement le minimum (descendre toujours à gauche), le maximum (toujours à droite), ou le successeur d'une valeur. Ce sont ces opérations ordonnées qui distinguent un BST d'une hashmap.

Le problème : la dégénérescence

Voici le défaut majeur du BST de base. Les opérations sont en O(hauteur), pas en O(log n). Et la hauteur dépend de l'ordre d'insertion. Si on insère des valeurs déjà triées (1, 2, 3, 4, 5), chaque nouvelle valeur va systématiquement à droite. L'arbre dégénère en une liste chaînée verticale, de hauteur n. La recherche devient O(n), on perd tout l'intérêt.

Insertion de 1,2,3,4,5 dans un BST naïf :

1
 \
  2
   \
    3
     \
      4
       \
        5      hauteur = n, recherche en O(n)

C'est un cas fréquent en pratique : les données arrivent souvent partiellement triées. Le BST naïf est donc fragile. C'est exactement le même piège que le pire cas du tri rapide : une entrée triée, censée être facile, devient le pire cas.

Cas Hauteur Recherche / insertion
Arbre équilibré log n O(log n)
Arbre dégénéré (insertion triée) n O(n)

La solution est de forcer l'arbre à rester équilibré quoi qu'il arrive, en réorganisant sa structure à chaque insertion. C'est exactement le rôle des arbres équilibrés, AVL et rouge-noir, qui font l'objet de l'article suivant.


Sources

  • Cormen, T. H., et al. (2009). Introduction to Algorithms (3rd ed.), Chapitre 12 "Binary Search Trees". MIT Press.
  • Sedgewick, R., & Wayne, K. (2011). Algorithms (4th ed.), Section 3.2 "Binary Search Trees". Addison-Wesley.
  • Knuth, D. E. (1998). The Art of Computer Programming, Vol. 3, Section 6.2.2. Addison-Wesley.

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.