Aller au contenu

Premières fonctions

On va considérer le type des arbres binaires 'a arbre_bin défini par :

type 'a arbre_bin = 
    | Nil 
    | Noeud of 'a arbre_bin * 'a * 'a arbre_bin

Question 1. Écrire des fonctions permettant de calculer la taille et la hauteur d'un arbre, ainsi qu'une fonction permettant de déterminer si un arbre est réduit à une feuille.

Tests
assert (taille Nil = 0);
assert (taille (Noeud(Nil,3,Nil)) = 1);
assert (taille (Noeud(Nil,'a',Nil)) = 1);
assert (taille (Noeud(Nil,'a',Noeud(Nil,'b',Nil))) = 2);
"Valide";;

assert (hauteur Nil = -1);
assert (hauteur (Noeud(Nil,3,Nil)) = 0);
assert (hauteur (Noeud(Nil,'a',Nil)) = 0);
assert (hauteur (Noeud(Nil,'a',Noeud(Nil,'b',Nil))) = 1);
"Valide";;

assert (not (feuille Nil));
assert (feuille (Noeud(Nil,3,Nil)));
assert (feuille (Noeud(Nil,'a',Nil)));
assert (not (feuille (Noeud(Nil,'a',Noeud(Nil,'b',Nil)))));
"Valide";;
Corrigé
let rec taille (a : 'a arbre_bin) : int = match a with
    | Nil -> 0
    | Noeud(g,x,d) -> 1 + taille g + taille d

let rec hauteur (a : 'a arbre_bin) : int = match a with
    | Nil -> -1
    | Noeud(g,x,d) ->
            1 + max (hauteur g) (hauteur d)

let feuille (a : 'a arbre_bin) : bool = match a with
    | Nil -> false
    | Noeud(g, x, d) -> g = Nil && d = Nil

Question 2. Dans cette question, on va identifier un nœud à son étiquette.

Déterminer des fonctions renvoyant, sous forme d'une liste d'étiquettes, les nœuds, les feuilles et les nœuds internes d'un arbre.

Allez regarder les tests si vous avez un doute sur ce qui doit être renvoyé.

Tests
assert(noeuds Nil = []);
assert(noeuds (Noeud(Nil,1,Nil)) = [1]);
let a = Noeud(
    Noeud(Nil, 2, Noeud(Nil, 3, Nil)),
    4, Noeud(Noeud(Nil, 5, Nil), 6,
        Noeud(Nil, 7, Nil))) in
assert(List.sort Stdlib.compare (noeuds a)
    = [2;3;4;5;6;7]);
"Valide";;

assert(feuilles Nil = []);
assert(feuilles (Noeud(Nil,1,Nil)) = [1]);
let a = Noeud(
    Noeud(Nil, 2, Noeud(Nil, 3, Nil)),
    4, Noeud(Noeud(Nil, 5, Nil), 6,
        Noeud(Nil, 7, Nil))) in
assert(List.sort Stdlib.compare (feuilles a)
= [3;5;7]);
"Valide";;

assert(noeuds_internes Nil = []);
assert(noeuds_internes (Noeud(Nil,1,Nil)) = []);
let a = Noeud(
    Noeud(Nil, 2, Noeud(Nil, 3, Nil)),
    4, Noeud(Noeud(Nil, 5, Nil), 6,
        Noeud(Nil, 7, Nil))) in
assert(List.sort Stdlib.compare 
    (noeuds_internes a)
= [2;4;6]);
"Valide";;
Corrigé
let rec noeuds (a : 'a arbre_bin) : 'a list = 
    match a with
    | Nil -> []
    | Noeud(g, x, d) ->
        x :: (noeuds g @ noeuds d)

let rec feuilles (a : 'a arbre_bin) : 'a list = 
    match a with
    | Nil -> []
    | Noeud(g, x, d) ->
        if feuille a then [x]
        else feuilles g @ feuilles d

let rec noeuds_internes (a : 'a arbre_bin) = 
    match a with
    | Nil -> []
    | Noeud(Nil, _, Nil) -> []
    | Noeud(g, x, d) ->
        x :: (noeuds_internes g @ noeuds_internes d)

Question 3. Écrire une fonction permettant de déterminer si un arbre binaire est strict.

Tests
assert(not (est_strict Nil));
assert(est_strict (Noeud(Nil, 1, Nil)));
let a = Noeud(
    Noeud(Nil, 2, Noeud(Nil, 3, Nil)),
    4, Noeud(Noeud(Nil, 5, Nil), 6,
        Noeud(Nil, 7, Nil))) in
assert(not (est_strict a));
let b = Noeud(
    Noeud(Nil, 2, Nil),
    4, Noeud(Noeud(Nil, 5, Nil), 6,
        Noeud(Nil, 7, Nil))) in
assert(est_strict b);
"Valide";;
Corrigé
let rec est_strict (a : 'a arbre_bin) : bool =
    match a with
    | Nil -> false
    | Noeud(Nil, x, Nil) -> true
    | Noeud(g, x, d) -> g <> Nil && d <> Nil && est_strict g && est_strict d

Question 4. Écrire une fonction qui renvoie les sous-arbres d'un arbre, en dupliquant les sous-arbres identiques.

Tests
assert(sous_arbres Nil = [ Nil ]);
let a = Noeud(Nil, 1, Nil) in
assert(List.sort Stdlib.compare (sous_arbres a)
    = [ Nil; Nil; a ]);
let b = Noeud(Nil, 2, Nil) in
let c = Noeud(a, 3, b) in
assert(List.sort Stdlib.compare
    (sous_arbres c) =
    [Nil; Nil; Nil; Nil; a; b; c]);
"Valide";;
Corrigé
let rec sous_arbres (a : 'a arbre_bin) : 'a arbre_bin list = 
    match a with
    | Nil -> [Nil]
    | Noeud(g, x, d) ->
       a :: (sous_arbres g @ sous_arbres d)

Pour accéder à un nœud de l'arbre, on descend en partant de la racine et en allant à gauche ou à droite. On peut donc représenter un tel chemin par une liste de déplacements :

type deplacement = Gauche | Droite
type chemin = deplacement list

Question 5. Écrire une fonction chemin_noeud : 'a arbre_bin -> chemin -> 'a option qui renvoie l'étiquette d'un nœud donné par son chemin. On utilise un type option en cas de chemin invalide.

Tests
let a = Noeud(
    Noeud(Nil, 2, Noeud(Nil, 3, Nil)),
    4, Noeud(Noeud(Nil, 5, Nil), 6,
        Noeud(Nil, 7, Nil))) in
assert(chemin_noeud a [Gauche; Gauche] = None);
assert(chemin_noeud a [] = Some 4);
assert(chemin_noeud a [Droite; Gauche] = Some 5);
assert(chemin_noeud a [Droite; Droite] = Some 7);
assert(chemin_noeud a [Droite; Droite; Droite] = None);
"Valide";;
Corrigé
let rec chemin_noeud (a : 'a arbre_bin) (l : chemin) : 'a option =
    match a, l with
    | Nil, _ -> None
    | Noeud(g,x,d), Gauche::q -> chemin_noeud g q
    | Noeud(g,x,d), Droite::q -> chemin_noeud d q
    | Noeud(g,x,d), [] -> Some x

Parcours

Sur le même modèle que les fonctions précédentes, on peut explorer un arbre en explorant à gauche puis à droite ses sous-arbres de manière récursive. Chaque nœud est donc vu trois fois :

  • une première fois quand on le découvre avant d'explorer son sous-arbre gauche
  • entre les deux explorations
  • enfin quand on a fini d'explorer son sous-arbre droit.

Question 6. Écrire des fonctions affiche_avant, affiche_milieu et affiche_apres qui parcourent et affichent les étiquettes d'un string arbre_bin selon les trois cas précédents. Note : il s'agit essentiellement du même code à une ligne près.

Tests
let t = Noeud( Noeud(Nil, "1", Nil), "2", 
    Noeud(Nil, "3", Noeud(Nil, "4", Nil))) in
affiche_avant t (* doit afficher 2134 *);;

let t = Noeud( Noeud(Nil, "1", Nil), "2", 
    Noeud(Nil, "3", Noeud(Nil, "4", Nil))) in
affiche_milieu t (* doit afficher 1234 *);;

let t = Noeud( Noeud(Nil, "1", Nil), "2", 
    Noeud(Nil, "3", Noeud(Nil, "4", Nil))) in
affiche_apres t (* doit afficher 1432 *);;
Corrigé
let rec affiche_avant a = match a with
    | Nil -> ()
    | Noeud(g,x,d) ->
        print_string x;
        affiche_avant g;
        affiche_avant d

let rec affiche_milieu a = match a with
    | Nil -> ()
    | Noeud(g,x,d) ->
        affiche_milieu g;
        print_string x;
        affiche_milieu d

let rec affiche_apres a = match a with
    | Nil -> ()
    | Noeud(g,x,d) ->
        affiche_apres g;
        affiche_apres d;
        print_string x

Première évaluation d'expression arithmétique

On va considérer des expressions comme \((2+3) \times 5\) représentées par des string arbre_bin ainsi :

let e = Noeud(
    Noeud( Noeud(Nil, "2", Nil), 
           "+", 
           Noeud(Nil, "3", Nil)), 
    "*", 
    Noeud(Nil, "5", Nil))

Question 7. Dessiner cet arbre. Évaluer les trois fonctions précédentes sur cet arbre. Qu'en déduire ?

Corrigé

La racine est étiquetée par "*", son fils gauche par "+" (avec les feuilles "2" et "3") et son fils droit est la feuille "5". On obtient :

  • affiche_avant e : *+235, c'est l'écriture préfixe de l'expression ;
  • affiche_milieu e : 2+3*5, c'est l'écriture infixe usuelle, mais les parenthèses ont disparu : on ne peut plus distinguer \((2+3)\times 5\) de \(2 + (3 \times 5)\) ;
  • affiche_apres e : 23+5*, c'est l'écriture postfixe, qu'on sait évaluer avec une pile (voir le TP sur les piles et files).

Les écritures préfixe et postfixe permettent de retrouver l'arbre, et donc l'expression, sans parenthèses ; ce n'est pas le cas de l'écriture infixe.

Question 8. Écrire une fonction d'évaluation qui calcule le résultat de cette expression.

On pourra utiliser int_of_string pour convertir une chaîne de caractères comme "42" en l'entier 42.

Tests
let e = Noeud(Noeud( Noeud(Nil, "2", Nil), "+", Noeud(Nil, "3", Nil)), "*", Noeud(Nil, "5", Nil)) in
assert(evalue e = 25);
"Valide";;
Corrigé
let rec evalue a = match a with
    | Nil -> failwith "Vide"
    | Noeud(Nil,x,Nil) -> int_of_string x
    | Noeud(g, x, d) ->
            let eg = evalue g in
            let ed = evalue d in
            match x with
            | "+" -> eg + ed
            | "*" -> eg * ed
            | _ -> failwith "Ope inconnue"

Arbres binaires stricts

Pour représenter des arbres dont les nœuds internes ont tous deux fils non vides, on va utiliser le type :

type ('a, 'b) arbre_strict = Feuille of 'a 
    | NoeudI of ('a, 'b) arbre_strict * 'b * ('a, 'b) arbre_strict

Ce type permet de représenter naturellement des expressions arithmétiques comme :

type operations = Plus | Mult
let e = NoeudI(
    NoeudI(Feuille 2, Plus, Feuille 3), 
    Mult, 
    Feuille 5)

Question 9. Écrire une fonction evalue : (int, operations) arbre_strict -> int qui évalue une telle expression.

Tests
let e = NoeudI(NoeudI( Feuille 2, Plus, Feuille 3), Mult, Feuille 5) in
assert(evalue e = 25);
"Valide";;
Corrigé
let rec evalue a = match a with
    | Feuille n -> n
    | NoeudI(g, x, d) ->
            let eg = evalue g in
            let ed = evalue d in
            match x with
            | Plus -> eg + ed
            | Mult -> eg * ed

Si on considère le type

type ('a, 'b) etiquette = F of 'a | N of 'b

on peut passer d'un ('a, 'b) arbre_strict à un ('a, 'b) etiquette arbre_bin et, dans certains cas, d'un ('a, 'b) etiquette arbre_bin à un ('a, 'b) arbre_strict.

Question 10. Écrire ainsi deux fonctions de conversion :

arbre_strict_vers_arbre_bin : ('a, 'b) arbre_strict -> ('a, 'b) etiquette arbre_bin
arbre_bin_vers_arbre_strict : ('a, 'b) etiquette arbre_bin -> ('a, 'b) arbre_strict option
Tests
let asvab = arbre_strict_vers_arbre_bin in
assert(asvab (Feuille 1) = Noeud(Nil, F 1, Nil));
assert(asvab (NoeudI(Feuille 'a', 1, NoeudI(Feuille 'b', 2, Feuille 'c')))
    = Noeud(Noeud(Nil,F 'a',Nil), N 1, Noeud(Noeud(Nil,F 'b',Nil), N 2, Noeud(Nil, F 'c', Nil))));
"Valide";;

let abvas = arbre_bin_vers_arbre_strict in
assert(None = abvas Nil);
assert(None = abvas (Noeud(Nil, N 1, Nil)));
assert(None = abvas (Noeud(Nil, N 1, Noeud(Nil, F 2, Nil))));
assert(Some (Feuille 1) = abvas (Noeud(Nil, F 1, Nil)));
assert(Some (NoeudI(Feuille 'a', 1, NoeudI(Feuille 'b', 2, Feuille 'c')))
    = abvas (Noeud(Noeud(Nil,F 'a',Nil), N 1, Noeud(Noeud(Nil,F 'b',Nil), N 2, Noeud(Nil, F 'c', Nil)))));
"Valide";;
Corrigé
let rec arbre_strict_vers_arbre_bin a =
    match a with
    | Feuille n -> Noeud(Nil, F n, Nil)
    | NoeudI(g, x, d) -> Noeud(arbre_strict_vers_arbre_bin g,
                            N x,
                            arbre_strict_vers_arbre_bin d)

let rec arbre_bin_vers_arbre_strict a =
    match a with
    | Noeud(Nil, F f, Nil) -> Some (Feuille f)
    | Noeud(g, N x, d) -> begin
            match arbre_bin_vers_arbre_strict g, arbre_bin_vers_arbre_strict d with
            | Some g', Some d' -> Some (NoeudI(g', x, d'))
            | _ -> None
        end
    | _ -> None

Dessin d'arbres

On va réaliser dans cette partie une fonction de dessin d'arbres avec la bibliothèque graphics. L'idée est de placer la racine et de dessiner les sous-arbres gauche et droit en dessous. Pour cela, il va falloir connaître la taille de ces sous-arbres en pixels.

On pourra consulter la documentation de graphics ici : Graphics

Question 11. Écrire une fonction pixels : 'a arbre_bin -> int * int qui renvoie un couple (largeur, racine) où largeur est la largeur en pixels d'un arbre dans son affichage et racine l'abscisse de la racine dans cet affichage.

Corrigé
let rnoeud = 5 (* Le rayon du noeud *)
let marge = 2 (* la marge autour du noeud *)

let rec pixels a = match a with
    | Nil -> 0, 0
    | Noeud(g, _, d) -> 
            let lg, _ = pixels g in
            let ld, _ = pixels d in
            lg + ld + 2*(marge + rnoeud), lg + marge + rnoeud

Question 12. En déduire une fonction dessine : string arbre_bin -> int -> int -> unit telle que dessine a x y dessine l'arbre \(a\) en plaçant la racine en \((x, y)\).

On pourra utiliser draw_string pour dessiner l'étiquette, et text_size pour connaître les dimensions en pixels d'une chaîne.

Corrigé
let rec dessine a x y =
    match a with
    | Nil -> ()
    | Noeud(g, e, d) ->
            (* On précalcule les tailles des sous-arbres *)
            let lg, rg = pixels g in
            let _, rd = pixels d in
            (* position de la racine gauche relativement à (x,y) *)
            let v_rg = x - lg - marge - rnoeud + rg in
            (* position de la racine droite relative (x, y) *)
            let v_rd = x + marge + rnoeud + rd in
            let dec_y = y - 2 * rnoeud - marge in

            moveto x y; lineto v_rg dec_y;
            moveto x y; lineto v_rd dec_y;
            fill_circle x y rnoeud;

            (* dimensions en pixels du texte de l'étiquette *)
            let tw, th = text_size e in
            moveto (x-tw/2) (y-th/2);
            set_color white;
            draw_string e;
            set_color black;

            dessine g v_rg dec_y;
            dessine d v_rd dec_y

Génération aléatoire d'arbres

Dans cette partie, ce qui nous intéresse est de générer aléatoirement des arbres pour en observer la structure. Les étiquettes ne sont pas pertinentes, on pourra donc au choix, soit considérer des unit arbre_bin, soit redéfinir un type d'arbres non étiquetés.

On va réaliser ici deux modèles de génération aléatoire d'arbres.

Premier modèle

Le premier modèle consiste, pour générer un arbre de \(n \ge 1\) nœuds, à choisir aléatoirement \(k\) dans \(\range{0}{n-1}\), à générer aléatoirement un arbre \(g\) à \(k\) nœuds et un arbre \(d\) à \(n-1-k\) nœuds puis à renvoyer l'arbre Noeud(g, (), d). Pour \(n = 0\), on renvoie l'arbre vide.

Question 13. Implémenter ce modèle avec une fonction genere_arbre_1 : int -> unit arbre_bin.

Tester votre fonction, notamment avec l'affichage de la partie précédente.

Corrigé

Random.int n renvoie un entier uniforme dans \(\range{0}{n-1}\).

let rec genere_arbre_1 n =
    if n = 0
    then Nil
    else begin
        let k = Random.int n in
        Noeud(genere_arbre_1 k, (), genere_arbre_1 (n - 1 - k))
    end

Second modèle

Pour le deuxième modèle, on va choisir un sous-arbre vide uniformément et le remplacer par un nœud.

Question 14. Écrire une fonction chemins_vides : 'a arbre_bin -> chemin list qui, étant donné un arbre, renvoie la liste des chemins permettant d'aboutir à un sous-arbre vide.

Corrigé
let rec chemins_vides a =
    match a with
    | Nil -> [ [] ]
    | Noeud(g, _, d) ->
        List.map (fun c -> Gauche :: c) (chemins_vides g)
        @ List.map (fun c -> Droite :: c) (chemins_vides d)

Un arbre à \(n\) nœuds a exactement \(n+1\) sous-arbres vides : la liste renvoyée est de longueur \(n+1\).

Question 15. Écrire une fonction remplit : unit arbre_bin -> chemin -> unit arbre_bin qui remplace un sous-arbre vide, donné par son chemin, par un nœud.

Corrigé
let rec remplit a c =
    match a, c with
    | Nil, [] -> Noeud(Nil, (), Nil)
    | Noeud(g, y, d), Gauche :: q -> Noeud(remplit g q, y, d)
    | Noeud(g, y, d), Droite :: q -> Noeud(g, y, remplit d q)
    | _ -> failwith "chemin invalide"

Question 16. En déduire une fonction genere_arbre_2 : int -> unit arbre_bin qui prend un entier n et construit un arbre à \(n\) nœuds en partant de l'arbre vide et en réalisant \(n\) remplacements d'un arbre vide par un nœud, le choix de l'arbre vide se faisant uniformément.

Note : on pourra utiliser List.nth et Random.int. Voir par exemple Random.

Corrigé
let genere_arbre_2 n =
    let a = ref Nil in
    for _ = 1 to n do
        let vides = chemins_vides !a in
        let c = List.nth vides (Random.int (List.length vides)) in
        a := remplit !a c
    done;
    !a