Vue d'ensemble
Un arbre binaire quelconque range des entiers sans règle : chercher une valeur oblige à visiter tous les nœuds. L'arbre binaire de recherche (ABR) ajoute une seule contrainte d'ordre, et cette contrainte change tout : chercher, insérer, lister trié deviennent naturels et rapides. En OCaml, on exploite le filtrage sur le type arbre et l'immutabilité : insérer ne modifie pas l'arbre, on en construit un nouveau.
- Propriété caractéristique d'un ABR (ordre gauche/droite).
- Recherche d'une valeur par filtrage récursif, coût où est la hauteur.
- Insertion respectant la propriété d'ABR, en renvoyant un nouvel arbre.
- Fait clé : le parcours infixe d'un ABR énumère les étiquettes par ordre croissant.
- Piège du déséquilibre : insertion de données déjà triées → peigne de hauteur .
Prérequis
- Le type
type arbre = Vide | Noeud of arbre * int * arbreet le filtrage (fiche ocaml-arbres-binaires). - Parcours d'arbre (préfixe, infixe, suffixe) et notion de hauteur.
- Récursivité structurelle et concaténation de listes
@. - Comparaisons
<,=,>sur les entiers.
Un ABR, ce n'est pas « un arbre en plus ». C'est la première structure où un invariant d'ordre rend un algorithme rapide — l'idée qu'on retrouve partout en info. Nos mentors alumni X · Centrale · Mines t'apprennent à raisonner sur l'invariant plutôt qu'à réciter du code.
Trouver un mentor →La propriété d'ABR
On part toujours du même type qu'en arbres binaires : une étiquette entière et deux sous-arbres.
type arbre = Vide | Noeud of arbre * int * arbretype arbre =on définit un type somme nommé arbre : une valeur de ce type est de l'une des formes listées après le =.Videpremier constructeur, sans argument : l'arbre vide (l'analogue de la liste vide []).| Noeud of arbre * int * arbresecond constructeur : un triplet (sous-arbre gauche, étiquette entière, sous-arbre droit). Le type est récursif car arbre apparaît dans sa propre définition.Un arbre binaire d'entiers est un ABR si, pour tout nœud d'étiquette :
- toutes les étiquettes du sous-arbre gauche sont < x ;
- toutes les étiquettes du sous-arbre droit sont > x.
La condition porte sur tout le sous-arbre, pas seulement sur les fils immédiats : c'est une propriété globale, vraie à chaque nœud.
Voici un arbre correct. À la racine : le sous-arbre gauche ne contient que (tous < 5), le droit ne contient que (> 5). Au nœud : à gauche 1 < 3, à droite rien. Chaque nœud respecte la règle.
(* 5
/ \
3 8
/
1 *)L'arbre ci-dessous a chaque fils bien placé par rapport à son père immédiat, mais ce n'est PAS un ABR : le est dans le sous-arbre gauche de , or 6 > 5. La règle exige que toutes les étiquettes de gauche soient < 5.
(* 5
/
3
\
6 6 > 5 mais placé à gauche de 5 : interdit *)Rechercher une valeur
La propriété d'ABR guide la descente : à chaque nœud, une seule comparaison indique le côté à explorer. On n'explore jamais les deux sous-arbres.
Chercher dans un ABR : si l'arbre est vide, est absent. Sinon, soit l'étiquette de la racine ; si c'est trouvé, si v < x on cherche uniquement dans le sous-arbre gauche, si v > x uniquement dans le droit.
let rec recherche v a =
match a with
| Vide -> false
| Noeud (g, x, d) ->
if v = x then true
else if v < x then recherche v g
else recherche v dlet rec recherche v a =rec car la fonction s'appelle elle-même ; v est la valeur cherchée, a l'arbre. Type inféré : int -> arbre -> bool.match a withon filtre sur la forme de l'arbre : soit vide, soit un nœud.| Vide -> falsecas de base : on est arrivé sur un arbre vide sans trouver v, donc v est absent.| Noeud (g, x, d) ->on nomme les trois composantes du nœud : g gauche, x l'étiquette, d droite.if v = x then trueégalité : la valeur est ici, on renvoie immédiatement true.else if v < x then recherche v gsi v est plus petit, la propriété d'ABR garantit qu'il ne peut être qu'à gauche : on descend dans g et on ignore d.else recherche v ddernier cas (v > x) : on descend uniquement à droite. Un seul chemin est parcouru, d'où l'efficacité.| Appel | nœud | comparaison | action |
|---|---|---|---|
| recherche 4 (racine) | 5 | 4 < 5 | descente à gauche |
| recherche 4 | 3 | 4 > 3 | descente à droite |
| recherche 4 | — | Vide | false : 4 absent |
| recherche 8 (racine) | 5 | 8 > 5 | descente à droite |
| recherche 8 | 8 | 8 = 8 | true : trouvé ✓ |
Chaque appel descend d'un niveau. Le nombre d'appels est donc borné par la hauteur de l'arbre : la recherche est en . Si l'arbre est équilibré, et la recherche est en . Tout dépend de la forme de l'arbre — voir le piège du déséquilibre.
Insérer une valeur
Insérer suit le même chemin que la recherche, jusqu'à tomber sur un Vide : c'est là qu'on greffe la nouvelle feuille. En OCaml l'arbre est immuable : on ne modifie rien, on reconstruit le chemin descendu en renvoyant un nouvel arbre.
let rec insere v a =
match a with
| Vide -> Noeud (Vide, v, Vide)
| Noeud (g, x, d) ->
if v = x then a
else if v < x then Noeud (insere v g, x, d)
else Noeud (g, x, insere v d)let rec insere v a =on insère v dans a et on renvoie le nouvel arbre. Type : int -> arbre -> arbre.| Vide -> Noeud (Vide, v, Vide)on a atteint une place libre : on crée une feuille portant v (deux sous-arbres vides). C'est le seul endroit où un nœud apparaît réellement.if v = x then av est déjà présent : on renvoie l'arbre inchangé (un ABR ne stocke pas de doublon).else if v < x then Noeud (insere v g, x, d)v doit aller à gauche : on reconstruit un nœud de même étiquette x et même sous-arbre droit d, mais avec le sous-arbre gauche mis à jour par l'appel récursif.else Noeud (g, x, insere v d)cas v > x : symétrique, on ne touche qu'au sous-arbre droit. Le sous-arbre g non concerné est partagé tel quel avec l'ancien arbre : rien n'est recopié inutilement.| Étape | chemin suivi | arbre obtenu (parenthésé) |
|---|---|---|
| insere 5 dans Vide | Vide → feuille | N(V, 5, V) |
| insere 3 | 3 < 5 → gauche (Vide) | N( N(V,3,V), 5, V ) |
| insere 8 | 8 > 5 → droite (Vide) | N( N(V,3,V), 5, N(V,8,V) ) |
| insere 1 | 1 < 5 → gauche ; 1 < 3 → gauche (Vide) | N( N( N(V,1,V), 3, V ), 5, N(V,8,V) ) ✓ |
L'arbre final (avec N pour Noeud et V pour Vide) a la forme :
(* 5
/ \
3 8
/
1 *)Chaque insertion crée seulement les nœuds situés sur le chemin descendu ; tous les sous-arbres écartés sont réutilisés sans copie. L'ancien arbre reste valide et intact. C'est la signature du style fonctionnel : on ne mute pas, on renvoie une nouvelle version qui partage l'essentiel avec l'ancienne.
« Renvoyer un nouvel arbre » déroute la plupart des MP2I. Là où en C on modifierait un pointeur en place, OCaml reconstruit le chemin — et le reste est partagé. Nos mentors alumni X · Centrale · Mines te font manipuler les deux visions pour que tu ne les confondes plus jamais.
Trouver un mentor →Le parcours infixe trie l'ABR
Rappel : le parcours infixe visite d'abord tout le sous-arbre gauche, puis la racine, puis tout le sous-arbre droit. Sur un ABR, cela produit les étiquettes dans l'ordre croissant : à chaque nœud, gauche < racine < droite, et cette règle se propage récursivement.
let rec infixe a =
match a with
| Vide -> []
| Noeud (g, x, d) -> infixe g @ [x] @ infixe dlet rec infixe a =renvoie la liste des étiquettes en ordre infixe. Type : arbre -> int list.| Vide -> []un arbre vide donne la liste vide.| Noeud (g, x, d) -> infixe g @ [x] @ infixe don concatène : la liste (triée) du gauche, puis le singleton [x], puis la liste (triée) du droit. Gauche < x < droite, donc le résultat est trié.infixe visite : gauche de 5 (= 1 puis 3), puis 5, puis droite de 5 (= 8). Résultat : [1; 3; 5; 8] — trié. Ce fait donne un algorithme de tri : insérer tous les éléments dans un ABR puis lire l'infixe.
Corollaire pratique : un arbre binaire est un ABR si et seulement si son parcours infixe est strictement croissant. C'est souvent la façon la plus simple de vérifier la propriété sur un exemple.
Le piège du déséquilibre
Le coût est excellent… tant que est petit. Or la forme de l'ABR dépend entièrement de l'ordre d'insertion, et rien dans insere ne rééquilibre l'arbre.
Insérons dans cet ordre. Chaque valeur est plus grande que toutes les précédentes : elle part toujours à droite. On obtient un peigne — une seule branche descendante :
(* 1
\
2
\
3
\
4 *)La hauteur vaut au lieu de . La recherche redevient : l'ABR ne vaut alors pas mieux qu'une liste chaînée. En vraie CPGE, on corrige cela avec des arbres équilibrés (hors programme MP2I), mais tu dois savoir reconnaître ce cas dégénéré.
Complexité de recherche/insertion : . Au mieux (arbre équilibré) ; au pire (peigne) , soit . La borne n'est jamais garantie par ce code seul.
Exercices corrigés
On considère l'ABR obtenu en insérant, dans cet ordre, dans un arbre vide.
a) Dessine l'arbre. b) Donne le résultat de infixe. c) La recherche de descend-elle à gauche ou à droite de la racine, et aboutit-elle ?
Voir la correction détaillée
(* 4
/ \
2 6
/ \
1 3 *)[1; 2; 3; 4; 6] — trié, comme attendu pour un ABR.Vide. Donc false : est absent.Écris une fonction minimum : arbre -> int qui renvoie la plus petite étiquette d'un ABR non vide, en exploitant la propriété d'ABR (sans parcourir tout l'arbre). On lèvera failwith "arbre vide" sur Vide.
Voir la correction détaillée
Vide porte le minimum.let rec minimum a =
match a with
| Vide -> failwith "arbre vide"
| Noeud (Vide, x, _) -> x
| Noeud (g, _, _) -> minimum gNoeud (Vide, x, _) filtre précisément un nœud sans fils gauche : son étiquette x est le minimum. Sinon (g non vide) on continue à gauche. Le _ ignore les composantes inutiles. Coût : .Vide → on renvoie 1. Correct.Écris est_abr : arbre -> bool qui teste si un arbre binaire est un ABR. On utilisera le corollaire : un arbre est un ABR si et seulement si son parcours infixe est strictement croissant. On pourra écrire une fonction auxiliaire croissante : int list -> bool.
Voir la correction détaillée
let rec croissante l =
match l with
| [] | [_] -> true
| a :: b :: reste -> a < b && croissante (b :: reste)[] | [_] : une liste de 0 ou 1 élément est trivialement croissante. Sinon on compare les deux premiers a et b : il faut a < b, puis on recommence à partir de b (on n'oublie pas de le remettre en tête avec b :: reste).
let est_abr a = croissante (infixe a)Récap final — Ce qu'il faut absolument retenir
L'ABR = un arbre binaire + un invariant d'ordre. Cet invariant se paie une fois (à l'insertion) et se récupère partout : recherche rapide et lecture triée gratuite.
- Sais-tu énoncer la propriété d'ABR (gauche < < droite, pour tout le sous-arbre, à chaque nœud) ?
- Sais-tu écrire
recherchepar filtrage, en ne descendant que d'un côté selon la comparaison ? - Sais-tu que la recherche coûte , et pourquoi (un seul chemin parcouru) ?
- Sais-tu écrire
inserequi renvoie un nouvel arbre en reconstruisant le chemin (immutabilité, partage des sous-arbres écartés) ? - Sais-tu qu'insérer une valeur déjà présente laisse l'arbre inchangé ?
- Sais-tu que le parcours infixe d'un ABR donne les étiquettes triées par ordre croissant ?
- Sais-tu reconnaître le peigne obtenu par insertion de données triées, et que sa hauteur vaut (recherche ) ?
- Sais-tu tester si un arbre est un ABR via la stricte croissance de son infixe ?