Adloun

Arbres

Cours complet · informatique (MP2I/MPI), chapitre 10 · MP2I et MPI

Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre

10.1 Une structure qui se définit par elle-même

Les structures du chapitre chap:sequentielles étaient linéaires : chaque élément avait au plus un suivant. L'arbre rompt avec cela — un nœud peut avoir plusieurs fils — et c'est ce qui lui donne son pouvoir : une recherche dans une liste de éléments coûte , dans un arbre bien formé .

C'est aussi le premier objet vraiment inductif du livre. Sa définition tient en deux règles, sa preuve en deux cas, son code en deux motifs. Tout ce qui a été posé au chapitre chap:induction s'applique ici sans adaptation.

10.2 Définition et vocabulaire

Définition 10.1Arbre binaire, par induction

L'ensemble des arbres binaires étiquetés par est le plus petit ensemble tel que :


type 'a arbre = Vide | Noeud of 'a arbre * 'a * 'a arbre

typedef struct noeud_s {
    int etiquette;
    struct noeud_s* gauche;    /* NULL joue le rôle de Vide */
    struct noeud_s* droit;
} noeud;
Définition 10.2Le vocabulaire, celui du programme
  • nœud : un Nœud de la définition ; racine : celui du sommet ;
  • feuille : un nœud dont les deux fils sont vides ; nœud interne : les autres ;
  • fils et père : et sont les fils de , qui est leur père ;
  • étiquette : la valeur portée par un nœud ;
  • sous-arbre : tout arbre atteint en descendant ;
  • profondeur d'un nœud : le nombre d'arcs qui le séparent de la racine — celle-ci est à la profondeur ;
  • hauteur de l'arbre : la plus grande profondeur atteinte.
AttentionLa hauteur de l'arbre vide vaut

C'est une convention, et le programme la fixe explicitement. Elle n'est pas arbitraire : elle est la seule qui rende vraie l'identité

dans tous les cas, y compris pour une feuille — dont les deux fils sont vides, et dont la hauteur vaut alors . Avec la convention , il faudrait un cas particulier pour les feuilles, et toutes les formules du chapitre porteraient une exception.

10.3 Les fonctions de base, et leurs preuves

Exemple 10.3Taille et hauteur

(* Nombre de nœuds de a. *)
let rec taille = function
  | Vide -> 0
  | Noeud (g, _, d) -> 1 + taille g + taille d

(* Hauteur de a ; celle de l'arbre vide vaut -1. *)
let rec hauteur = function
  | Vide -> -1
  | Noeud (g, _, d) -> 1 + max (hauteur g) (hauteur d)

Terminaison : les appels portent sur et , strictement plus petits pour l'ordre induit, qui est bien fondé. Complexité : chaque nœud est visité exactement une fois, donc pour un arbre de nœuds — et en espace de pile.

Proposition 10.4Encadrement du nombre de nœuds

Pour un arbre binaire de hauteur et de taille :

Démonstration

Par induction structurelle. Pour , avec et , l'encadrement s'écrit : il tient, et c'est pourquoi l'énoncé part de et non de .

Soit , de hauteur et de taille .

Minoration. Supposons , donc . Par hypothèse d'induction , et . Donc .

Majoration. Par hypothèse d'induction, puisque , et de même pour . Donc

ImportantLa conséquence qui gouverne tout le chapitre suivant

En inversant la majoration : . La hauteur d'un arbre à nœuds vaut donc au mieux , et au pire — le cas du peigne, où chaque nœud n'a qu'un fils, et qui n'est rien d'autre qu'une liste chaînée déguisée.

Comme toutes les opérations de recherche coûteront , tout l'enjeu du chapitre chap:tas sera d'empêcher l'arbre de dégénérer.

iRemarqueL'arbre complet dans un tableau

Le programme demande de « mentionner la représentation d'un arbre complet dans un tableau ». Si l'arbre est complet — tous les niveaux remplis, le dernier tassé à gauche — on peut se passer de pointeurs : en rangeant la racine à l'indice , les fils du nœud d'indice sont aux indices et , et son père à . Aucun pointeur, aucune allocation, une mémoire contiguë : c'est la représentation qu'utilisera le tas au chapitre chap:tas.

10.4 Arbres d'arité quelconque

Définition 10.5Arbre général

Un arbre (sans qualificatif) autorise un nombre quelconque de fils par nœud.


type 'a arbre_general = N of 'a * 'a arbre_general list

Méthode : Conversion « fils gauche, frère droit »

Le programme demande la « conversion d'un arbre d'arité quelconque en un arbre binaire ». Le procédé est classique et se dit en une phrase : le fils gauche du binaire est le premier fils du général ; le fils droit est le frère suivant.


let rec vers_binaire (N (e, fils)) = Noeud (freres fils, e, Vide)
and freres = function
  | [] -> Vide
  | N (e, f) :: reste -> Noeud (freres f, e, freres reste)

La conversion est bijective, et elle permet de n'écrire qu'une fois les algorithmes sur les arbres. En revanche elle allonge : trois frères deviennent une chaîne de hauteur trois.

iRemarqueÀ quoi servent les arbres

Le programme laisse les illustrations « au choix du professeur » et en cite plusieurs : expressions arithmétiques, arbres préfixes (trie), arbres de décision, dendrogrammes, arbres de classification. On les retrouvera : l'arbre de décision au chapitre chap:apprentissage, le dendrogramme de la classification hiérarchique au même endroit, l'arbre syntaxique au chapitre chap:grammaires, et l'arbre de preuve au chapitre chap:deduction.

Exemple 10.6L'expression arithmétique comme arbre

type expr =
  | Cst of int
  | Plus of expr * expr
  | Fois of expr * expr

(* Évalue e. La structure de la fonction EST la structure du type. *)
let rec evalue = function
  | Cst n -> n
  | Plus (a, b) -> evalue a + evalue b
  | Fois (a, b) -> evalue a * evalue b

L'arbre porte la priorité des opérateurs dans sa forme : et sont deux arbres différents, et aucune parenthèse n'est nécessaire. C'est précisément ce qu'un analyseur syntaxique construit — chapitre chap:grammaires.

10.5 Parcours

Définition 10.7Trois ordres de parcours en profondeur

Un parcours visite chaque nœud une fois. Pour un arbre binaire, la question est quand traiter la racine par rapport à ses sous-arbres :

  • préfixe : racine, puis gauche, puis droit ;
  • infixe : gauche, puis racine, puis droit ;
  • postfixe : gauche, puis droit, puis racine.

let rec prefixe = function
  | Vide -> []
  | Noeud (g, e, d) -> e :: (prefixe g @ prefixe d)

let rec infixe = function
  | Vide -> []
  | Noeud (g, e, d) -> infixe g @ (e :: infixe d)

let rec postfixe = function
  | Vide -> []
  | Noeud (g, e, d) -> postfixe g @ postfixe d @ [e]
Exemple 10.8Les trois parcours de l'arbre de la page précédente
préfixe8, 3, 1, 6, 4, 9, 7
infixe1, 3, 4, 6, 8, 7, 9
postfixe1, 4, 6, 3, 7, 9, 8
AttentionCes trois fonctions sont en , et c'est le `@` qui coûte

La concaténation @ recopie sa liste de gauche (chapitre chap:langage-ocaml). Sur un peigne gauche, le coût devient quadratique. La bonne écriture passe un accumulateur :


(* Renvoie la liste infixe de a, suivie de acc. Complexité : O(n). *)
let rec infixe_acc a acc =
  match a with
  | Vide -> acc
  | Noeud (g, e, d) -> infixe_acc g (e :: infixe_acc d acc)

Chaque nœud n'entraîne qu'un ::, en : le parcours redevient . C'est une illustration exacte de la mise en garde du chapitre chap:langage-ocaml : une complexité peut être ruinée par une seule opération mal choisie.

iRemarqueLe parcours et la pile d'appels

Le programme suggère d'« évoquer le lien avec l'empilement de blocs d'activation lors de l'appel à une fonction récursive ». Le parcours préfixe empile la racine avant de descendre, le postfixe la traite après être remonté : ce sont exactement la descente et la remontée de la pile d'exécution du chapitre chap:recursivite. Un parcours en profondeur écrit avec une pile explicite et le même parcours écrit récursivement font littéralement la même chose — l'un avec la pile du programme, l'autre avec celle de la machine.

ImportantLe parcours infixe d'un arbre de recherche est trié

C'est le résultat le plus utile de la section, et il sera démontré au chapitre suivant. Il donne déjà l'intuition : dans l'arbre dessiné plus haut, la ligne infixe est — presque triée, mais pas tout à fait, car cet arbre n'est pas un arbre binaire de recherche. Un arbre où elle le serait exactement mérite un nom, et c'est le sujet du chapitre chap:tas.

10.6 Ce qu'il faut retenir

ImportantArbres : quatre points
  • Le type est inductif : deux constructeurs, donc deux motifs, donc deux cas de preuve. La terminaison est acquise par l'ordre induit.
  • , et : la hauteur est entre et .
  • Toute opération de recherche coûtera : équilibrer, c'est tout gagner.
  • Trois parcours en profondeur — préfixe, infixe, postfixe — et l'on écrit avec un accumulateur, jamais avec @.

Continuer sur Adloun : animation, QCM, fiches, exercices