Adloun

Types sommes et arbres

Cours complet · OCaml (option informatique), chapitre 3 · prépas MPSI et MP, option informatique

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

<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>3.1 Introduction et motivation

Jusqu'ici nous avons utilisé les types qu'OCaml fournit : entiers, booléens, n-uplets, listes, option. La puissance du langage vient de ce qu'on peut définir les siens, taillés sur mesure pour le problème traité. Modéliser un jeu de cartes, une expression arithmétique, un arbre généalogique : à chaque fois, un type bien choisi rend le code plus clair, et surtout permet au compilateur de vérifier qu'on a traité tous les cas.

Ce chapitre présente les types sommes (aussi appelés types énumérés, ou unions) : un type dont une valeur est l'une parmi plusieurs formes, chacune introduite par un constructeur. Le filtrage du chapitre précédent s'y applique directement et y prend tout son sens : un match sur un type somme est exhaustif exactement quand il couvre tous les constructeurs — et le compilateur le vérifie. On découvrira enfin les types récursifs, qui se définissent à partir d'eux-mêmes : les arbres, structure aussi fondamentale que la liste, et qui se traite par le même réflexe de récursion.

3.2 Déclarer un type

Le mot-clé type introduit un nouveau nom de type. Dans le cas le plus simple, un synonyme pour un type existant, qui documente l'intention :


type point = float * float
type chemin = point list

Un type peut être polymorphe : il dépend d'un (ou plusieurs) type(s) paramètre(s), notés 'a, 'b, …, écrits avant le nom.


type 'a paire = 'a * 'a          (* paire homogène *)

3.3 Les types sommes

3.3.1 Types énumérés

La forme la plus simple d'un type somme énumère un nombre fini de constructeurs constants :


type couleur = Trefle | Carreau | Coeur | Pique
ImportantConstructeurs en majuscule

Les noms de constructeurs commencent obligatoirement par une majuscule (Trefle, Coeur), ce qui les distingue d'un coup d'œil des identifiants (variables, fonctions), qui commencent par une minuscule. Le séparateur entre constructeurs est la barre verticale |.

Une valeur de type couleur s'examine par filtrage : un match est exhaustif s'il couvre les quatre constructeurs.

Exemple 3.1Couleur rouge ou noire

let est_rouge c =
  match c with
  | Coeur | Carreau -> true
  | Trefle | Pique -> false

Un même résultat peut regrouper plusieurs motifs avec | (« motif ou »). Si l'on ajoutait plus tard un constructeur au type couleur, le compilateur signalerait aussitôt que ce match n'est plus exhaustif : une aide précieuse pour faire évoluer un programme sans rien oublier.

3.3.2 Constructeurs porteurs de données

Un constructeur peut transporter des valeurs, déclarées après le mot-clé of :


type forme =
  | Point
  | Cercle of float                 (* rayon *)
  | Rectangle of float * float      (* largeur, hauteur *)

Ainsi Cercle 2.0 et Rectangle (3.0, 4.0) sont deux valeurs du même type forme. Le filtrage récupère au passage les données portées par chaque constructeur :


let aire f =
  match f with
  | Point -> 0.0
  | Cercle r -> 3.14159 *. r *. r
  | Rectangle (l, h) -> l *. h
iRemarque

Un type somme rend certains états impossibles à représenter : une forme est un point, un cercle ou un rectangle, jamais « un cercle avec une largeur et une hauteur ». Bien choisir ses constructeurs, c'est interdire à la racine les combinaisons absurdes — une grande source de robustesse.

3.3.3 Deux vieilles connaissances

Les types du chapitre précédent ne sont, en réalité, que des types sommes. Le type option est défini dans la bibliothèque standard par :


type 'a option = None | Some of 'a

None est un constructeur constant, Some porte une valeur de type 'a : c'est exactement le type somme polymorphe à deux cas qu'on filtrait au chapitre 2.

3.4 Les types récursifs : les arbres

Un type somme peut se référer à lui-même : il est alors récursif. C'est ainsi qu'on définit les structures de taille non bornée. La liste en est un exemple ; on pourrait la redéfinir nous-mêmes :


type 'a maliste = Vide | Cons of 'a * 'a maliste

Cons porte une tête et… une maliste (la queue) : la définition s'appuie sur le type en cours de définition. C'est exactement [] / ::, sans le sucre syntaxique.

3.4.1 Les arbres binaires

La structure récursive reine, après la liste, est l'arbre binaire : un arbre est vide, ou bien un nœud portant une valeur (l'étiquette) et deux sous-arbres.

Définition 3.2Arbre binaire

type 'a arbre =
  | Vide
  | Noeud of 'a arbre * 'a * 'a arbre   (* sous-arbre gauche, étiquette, droit *)

Vide est l'arbre sans nœud ; Noeud (g, x, d) a pour racine l'étiquette x, pour fils gauche g et pour fils droit d, tous deux des arbres. Une feuille est un nœud à deux fils vides : Noeud (Vide, x, Vide).

Exemple 3.3Construire un arbre

let a =
  Noeud (Noeud (Vide, 1, Vide),
         2,
         Noeud (Vide, 3, Vide))

Cet arbre a pour racine 2, et pour fils les deux feuilles 1 et 3.

3.4.2 Récursion sur les arbres

Le réflexe est le même que sur les listes — un filtrage sur les deux formes — mais le cas récursif comporte deux appels, un par sous-arbre.

Méthode : Traiter un arbre par récursion

On filtre selon les deux constructeurs :

  • cas de base Vide : que vaut le résultat sur l'arbre vide ?
  • cas récursif Noeud (g, x, d) : comment combiner l'étiquette x avec les résultats des appels récursifs sur g et sur d ?

La terminaison est garantie : les sous-arbres sont strictement plus petits.

Exemple 3.4Taille d'un arbre

let rec taille a =
  match a with
  | Vide -> 0
  | Noeud (g, _, d) -> 1 + taille g + taille d

On compte 1 pour le nœud courant, plus les tailles des deux sous-arbres. L'étiquette n'intervient pas (d'où _).

<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>3.5 Exercices résolus

Niveau (application directe du cours)

Exercice 1 : Jours de la semaine

Déclarer un type énuméré jour (les sept jours), puis écrire est_weekend : jour -&gt; bool.

Démonstration

type jour = Lundi | Mardi | Mercredi | Jeudi | Vendredi | Samedi | Dimanche

let est_weekend j =
  match j with
  | Samedi | Dimanche -> true
  | _ -> false

Le motif _ regroupe les cinq jours ouvrés. On pourrait aussi tous les énumérer ; le _ est ici justifié car le résultat est le même pour tous, mais attention : avec _, le compilateur ne préviendra plus si l'on ajoute un jour. À doser.

Exercice 2 : Aire d'une forme

Avec le type forme (Point, Cercle of float, Rectangle of float * float), écrire aire puis est_plus_grande f g qui teste si f a une aire strictement plus grande que g.

Démonstration

let aire f =
  match f with
  | Point -> 0.0
  | Cercle r -> 3.14159 *. r *. r
  | Rectangle (l, h) -> l *. h

let est_plus_grande f g = aire f > aire g

On réutilise aire dans est_plus_grande : une fonction bien nommée devient une brique. Les opérateurs sont flottants (*.), la comparaison &gt; polymorphe.

Exercice 3 : Taille d'un arbre

Écrire taille et l'appliquer à l'arbre Noeud (Noeud (Vide, 1, Vide), 2, Noeud (Vide, 3, Vide)).

Démonstration

let rec taille a =
  match a with
  | Vide -> 0
  | Noeud (g, _, d) -> 1 + taille g + taille d

Sur l'arbre proposé : 1 + taille (feuille 1) + taille (feuille 3) 1 + 1 + 1 = 3, chaque feuille valant 1 + 0 + 0.

Niveau (raisonnement intermédiaire)

Exercice 4 : Hauteur d'un arbre

Écrire hauteur a : pour l'arbre vide, et de plus que la hauteur du plus haut des deux sous-arbres sinon.

Démonstration

let rec hauteur a =
  match a with
  | Vide -> 0
  | Noeud (g, _, d) ->
      let hg = hauteur g and hd = hauteur d in
      1 + (if hg > hd then hg else hd)

On nomme les deux hauteurs (let ... and ...) pour ne les calculer qu'une fois chacune, puis on prend la plus grande. L'arbre vide a hauteur 0, une feuille hauteur 1.

Exercice 5 : Compter les feuilles

Écrire nb_feuilles a, le nombre de feuilles (nœuds à deux sous-arbres vides).

Démonstration

let rec nb_feuilles a =
  match a with
  | Vide -> 0
  | Noeud (Vide, _, Vide) -> 1
  | Noeud (g, _, d) -> nb_feuilles g + nb_feuilles d

On distingue trois cas par filtrage : l'arbre vide ( feuille), la feuille (Noeud (Vide, _, Vide), qui en est une), et le nœud interne (somme des feuilles des deux sous-arbres). L'ordre importe : le motif feuille, plus particulier, doit précéder le motif général Noeud (g, _, d).

Exercice 6 : Miroir d'un arbre

Écrire miroir a qui renvoie l'arbre symétrique (fils gauche et droit échangés, récursivement).

Démonstration

let rec miroir a =
  match a with
  | Vide -> Vide
  | Noeud (g, x, d) -> Noeud (miroir d, x, miroir g)

Le miroir de l'arbre vide est vide ; sinon on garde l'étiquette et on échange les deux sous-arbres après les avoir eux-mêmes mis en miroir. La fonction renvoie un nouvel arbre, sans modifier l'original (les valeurs sont immuables).

Exercice 7 : Évaluer une expression arithmétique

On modélise une expression par le type


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

Écrire evalue : expr -&gt; int et l'appliquer à Plus (Const 2, Fois (Const 3, Const 4)).

Démonstration

let rec evalue e =
  match e with
  | Const n -> n
  | Plus (a, b) -> evalue a + evalue b
  | Fois (a, b) -> evalue a * evalue b

Une expression est un arbre : les feuilles sont les constantes, les nœuds les opérations. L'évaluation suit la structure : evalue (Plus (Const 2, Fois (Const 3, Const 4))) 2 + (3 * 4) = 14. C'est le cœur d'un interpréteur.

Niveau (approfondissement)

Exercice 8 : Appartenance dans un arbre binaire de recherche

Un arbre binaire de recherche (ABR) est un 'a arbre tel que, en tout nœud, toutes les étiquettes du sous-arbre gauche sont inférieures à l'étiquette, et celles du sous-arbre droit supérieures. Écrire appartient x a qui exploite cette propriété pour ne descendre que d'un côté.

Démonstration

let rec appartient x a =
  match a with
  | Vide -> false
  | Noeud (g, v, d) ->
      if x = v then true
      else if x < v then appartient x g
      else appartient x d

La propriété d'ABR permet, à chaque nœud, d'éliminer la moitié de l'arbre : si x &lt; v, inutile de chercher à droite. Sur un arbre équilibré de nœuds, la recherche coûte — bien mieux que le parcours linéaire d'une liste.

Exercice 9 : Insertion dans un ABR

Écrire insere x a qui renvoie un nouvel ABR contenant x (sans modifier a), en préservant la propriété d'ABR. Une valeur déjà présente laisse l'arbre inchangé.

Démonstration

let rec insere x a =
  match a with
  | Vide -> Noeud (Vide, x, Vide)
  | Noeud (g, v, d) ->
      if x = v then a
      else if x < v then Noeud (insere x g, v, d)
      else Noeud (g, v, insere x d)

On insère à la place du premier Vide rencontré en suivant la règle de comparaison. La reconstruction Noeud (insere x g, v, d) ne recopie que le chemin de la racine au point d'insertion : le reste de l'arbre est partagé, non dupliqué. C'est la version persistante (immuable) d'un ABR.

Exercice 10 : Parcours infixe

Écrire infixe : 'a arbre -&gt; 'a list qui liste les étiquettes en ordre gauche – racine – droite. Que vaut ce parcours sur un ABR ? Discuter le coût.

Démonstration

let rec infixe a =
  match a with
  | Vide -> []
  | Noeud (g, x, d) -> infixe g @ [x] @ infixe d

Sur un ABR, le parcours infixe renvoie les étiquettes triées par ordre croissant — c'est une propriété caractéristique. Côté coût, les @ répétés rendent ce parcours quadratique dans le pire cas ; une variante à accumulateur (aux g (x :: aux d acc)) le ramène en — même idée que le renverse du chapitre 2.

Synthèse du chapitre (à retenir)
  • type déclare un nom de type : synonyme, polymorphe ('a), ou somme.
  • Type somme : plusieurs constructeurs séparés par |, en majuscule ; constants (Trefle) ou porteurs de données via of (Cercle of float).
  • On examine un type somme par filtrage : exhaustif ssi tous les constructeurs sont couverts ; ajouter un constructeur fait signaler tous les match devenus incomplets.
  • option (None | Some of 'a) et la liste (Vide | Cons of 'a ...) sont* des types sommes.
  • Type récursif : un constructeur référence le type en cours — d'où les structures non bornées. Arbre binaire : type 'a arbre = Vide | Noeud of 'a arbre 'a 'a arbre ; feuille Noeud (Vide, x, Vide).
  • Récursion sur arbre : cas Vide et cas Noeud (g, x, d) avec deux appels (sur g et d) ; terminaison par décroissance des sous-arbres.
  • ABR : gauche racine droite recherche/insertion en si équilibré ; parcours infixe étiquettes triées.

3.6 Exercices d'entraînement

Légende : application directe, raisonnement intermédiaire, approfondissement ; signale un classique incontournable. La numérotation prolonge celle des dix exercices résolus.

Thème A — Types énumérés et sommes simples.
Thème B — Mesures sur les arbres.
Thème C — Transformer des arbres.
Thème D — Arbres binaires de recherche.

Continuer sur Adloun : animation, QCM, fiches, exercices