Il est fortement conseillé de compiler le code OCaml (avec ocamlopt), plutôt que d'utiliser le mode interactif d'OCaml (ocaml). Si vous travaillez sur un fichier file.ml, vous pouvez le compiler avec la commande ocamlopt -o file file.ml et l'exécuter ensuite avec la commande ./file. Il est également suggérer d'utiliser un éditeur où OCaml est bien intégré (coloration, indentation, signalement des erreurs, etc.).
Si vous utilisez VSCode :
ocamlopt insertion.ml -o insertion && ./insertion(ou make si on utilise un Makefile, ou dune runtest si on utilise dune, etc.)
Ces TP comporte trois exercices indépendants. Ceux d'entre vous qui se sentent à l'aise avec OCaml mais souhaitent tout de même faire l'exercice 1 sur les tris peuvent attaquer directement à la question tri par tas.
Il est suggéré d'écrire chaque tri dans un fichier différent.
Écrire une fonction insertion_sort : int list -> int list qui réalise le tri par insertion d'une liste d'entiers. On rappelle que cela consiste à extraire le premier élément de la liste, à trier récursivement le reste de la liste, puis à insérer l'élément isolé (avec la fonction insertion).
On pourra tester avec le code suivant :
let rec print fmt = function | [] -> () | [x] -> Format.fprintf fmt "%d" x | x :: l -> Format.fprintf fmt "%d, %a" x print l let rec is_sorted = function | [] | [_] -> true | x :: (y :: _ as l) -> x <= y && is_sorted l let check l = let r = insertion_sort l in let ok = is_sorted r in Format.printf "[%a] => [%a]: %s@." print l print r (if ok then "OK" else "FAILED"); if not ok then exit 1 let () = check [1; 2; 3]; check [3; 2; 1]; check []; check [1]; check [2; 1; 1]
Écrire une fonction merge : int list -> int list -> int list qui prend en arguments deux listes supposées triées en ordre croissant et les fusionne en une unique liste triée.
En utilisant les deux fonctions précédentes, écrire une fonction mergesort : int list -> int list réalisant le tri fusion. On rappelle que le principe consiste à couper la liste en deux, à trier récursivement chacune des deux listes obtenues, puis à fusion les deux listes triées.
Tester comme pour le tri par insertion (en remplaçant bien sûr insertion_sort par mergesort).
Écrire une fonction quicksort : int list -> int list réalisant le tri rapide (quicksort). On rappelle que le principe consiste à isoler un élément particulier de la liste (ici le premier), à partionner les éléments restants en utilisant la fonction partition ci-dessus, à trier récursivement les deux listes obtenues, et enfin à recoller les morceaux. Pour cette dernière étape on pourra utiliser la fonction de bibliothèque List.append qui effectue la concaténation de deux listes (elle se note également @ de manière infixe).
Tester.
1 / \ 2 5 / \ 4 2 \ 6
Le but de cette question est de réaliser les deux opérations élémentaires sur la structure de tas que sont l'insertion d'un nouvel élément et l'extraction du plus petit élément (c'est-à-dire la racine). On se donne le type heap suivant pour représenter les tas :
type heap = Null | Fork of int * heap * heapOn commence par une opération plus complexe, merge : heap -> heap -> heap, qui réalise la fusion de deux tas a et b. Le principe est le suivant. Si a ou b est Null, le résultat est immédiat. Sinon, on compare la racine de a et celle de b ; supposons que la racine de a soit la plus petite (sinon, on échange les rôles de a et b). Le résultat est alors un tas dont la racine est celle de a, dont le sous-arbre gauche est le sous-arbre droit de a, et dont le sous-arbre droit est obtenu en fusionnant récursivement le sous-arbre gauche de a et l'arbre b. Écrire la fonction merge.
À l'aide de la fonction merge, écrire une fonction add : int -> heap -> heap qui insère un nouvel élément dans un tas. (On pourra se servir du tas Fork (x, Null, Null) qui contient uniquement x.)
De même, utiliser la fonction merge pour écrire une fonction extract_min : heap -> int * heap qui extrait le plus petit élément d'un tas (sa racine) et renvoie le tas constitué par tous les autres éléments. Lorsque le tas est vide, on pourra lever l'exception Empty_heap ainsi déclarée :
exception Empty_heap
Note : ces tas s'appellent des skew heaps et sont à la fois persistants, élégants et très efficaces.
On pourra tester avec le code suivant :
let rec print_heap fmt = function | Null -> Format.fprintf fmt "Null" | Fork (x, a, b) -> Format.fprintf fmt "Fork (@[%d,@ %a,@ %a@])" x print_heap a print_heap b let rec is_heap_rec min = function | Null -> true | Fork (x, l, r) -> min <= x && is_heap_rec x l && is_heap_rec x r let is_heap = is_heap_rec min_int let check_heap h = let ok = is_heap h in Format.printf "%a: %s@." print_heap h (if ok then "OK" else "FAILED"); if not ok then exit 1 let () = let h1 = add 2 (add 1 (add 3 Null)) in check_heap h1; let h2 = add 4 (add 0 (add 1 Null)) in check_heap h2; let h = merge h1 h2 in check_heap h; let m, h = extract_min h in assert (m = 0); check_heap h
Pour cela, commencer par écrire une fonction heap_of_list : int list -> heap qui construit un tas avec tous les éléments d'une liste.
Écrire ensuite la fonction inverse list_of_heap : heap -> int list qui construit une liste triée avec tous les éléments d'un tas.
Enfin, combiner les deux fonctions précédentes en une fonction heapsort : int list -> int list réalisant le tri par tas.
Tester comme pour les autres tris.
Les fonctions de tri précédentes utilisent la fonction de comparaison prédéfinie d'OCaml. On souhaite les généraliser afin qu'elles s'appliquent à des listes dont les éléments sont munis d'une relation d'ordre arbitraire.
sort : ('a -> 'a -> bool) -> 'a list -> 'a listoù la fonction passée en argument correspond à la relation « inférieur ou égal à ».
On pourra tester ainsi :
let rec print fmt = function | [] -> () | [x] -> Format.fprintf fmt "%d" x | x :: l -> Format.fprintf fmt "%d, %a" x print l let rec is_sorted le = function | [] | [_] -> true | x :: (y :: _ as l) -> le x y && is_sorted le l let check le l = let r = sort le l in let ok = is_sorted le r in Format.printf "[%a] => [%a]: %s@." print l print r (if ok then "OK" else "FAILED"); if not ok then exit 1 let () = check (<=) [1; 2; 3]; check (<=) [3; 2; 1]; check (<=) []; check (<=) [1]; check (<=) [2; 1; 1]; check (>=) [1; 2; 3]; check (>=) [3; 2; 1]; check (>=) []; check (>=) [1]; check (>=) [2; 1; 1]
module Sort(X : sig type t val le : t -> t -> bool end) : sig val sort : X.t list -> X.t list end = struct let sort l = ... endOn pourra tester avec des entiers, ainsi :
module S = Sort(struct type t = int let le = (<=) end);; let rec print fmt = function | [] -> () | [x] -> Format.fprintf fmt "%d" x | x :: l -> Format.fprintf fmt "%d, %a" x print l let rec is_sorted le = function | [] | [_] -> true | x :: (y :: _ as l) -> le x y && is_sorted le l let check l = let r = S.sort l in let ok = is_sorted (<=) r in Format.printf "[%a] => [%a]: %s@." print l print r (if ok then "OK" else "FAILED"); if not ok then exit 1 let () = check [1; 2; 3]; check [3; 2; 1]; check []; check [1]; check [2; 1; 1]
Par exemple, en tapant successivement les touches 2, 6, 6, 5, 6, 8 et 7 vous obtenez le mot bonjour. Il est possible qu'une suite de touches corresponde à plusieurs mots. Ainsi, la suite 5, 6, 4 et 3 correspond aux mots joie et loge et la suite 2, 5, 3 et 3 à clef et bled.
Le but de cet exercice est de programmer une solution efficace pour la recherche des mots du dictionnaire qui correspondent à une séquence de touches. Pour cela, nous allons représenter le dictionnaire par un arbre préfixe (en anglais trie) c'est-à-dire un arbre où chaque branche est étiquetée par le numéro d'une touche du téléphone et chaque noeud par les mots du dictionnaire correspondant à la séquence des touches menant de la racine de l'arbre à ce noeud.
Par exemple, l'arbre que l'on obtient pour le petit dictionnaire d'informatique composé des mots {hd, if, in, get, to, void, unit} est le suivant :
{} / \ 4/ \8 / \ {} {} / \ \ 6/ \3 \6 / \ \ {in} {if,hd} {to} | | |8 |4 | | {get} { } / \ 3/ \8 / \ {void} {unit}Pour représenter un tel arbre préfixe, on se donne le type OCaml suivant :
type trie = { words : string list ; branches : (int * trie) list }i.e. chaque noeud de l'arbre est un enregistrement où le champ words contient la liste des mots correspondant à ce noeud et où le champ branches est une liste de paires associant des arbres préfixes à des entiers (on appelle précisément cela une liste d'association).
change_assoc : 'a -> 'b -> ('a * 'b) list -> ('a * 'b) listqui modifie (ou étend) une liste d'association.
let dict = [ "lex"; "key"; "void" ; "caml" ; "unix" ; "for" ; "while" ; "done" ; "let" ; "fold"; "val"; "exn" ; "rec" ; "else" ; "then" ; "type" ; "try" ; "with" ; "to"; "find" ; "do" ; "in" ; "if" ; "hd" ; "tl" ; "iter" ; "map" ; "get"; "copy" ; "and"; "as" ; "begin" ; "class" ; "downto" ; "end" ; "exception" ; "false" ; "fun" ; "function" ; "match" ; "mod" ; "mutable" ; "open" ; "or" ; "true" ; "when" ; "load" ; "mem" ; "length" ; "bash" ; "unit" ; "site"; "php"; "sql"; "ssh"; "spam"; "su"; "qt"; "root"; "bsd"; "boot"; "bash"; "ocaml"; "kde"; "gtk" ; "gcc" ](On pourra utiliser pour cela la fonction de bibliothèque List.fold_left.)
Tester avec le code suivant :
let () = assert (find t [2;2;6;5] = ["caml"]) let () = assert (let l = find t [4;3] in l = ["hd"; "if"] || l = ["if"; "hd"]) let () = assert (find t [4;3;8] = ["get"]) let () = assert (find t [8;6;4;3] = ["void"]) let () = assert (find t [8;6;4;8] = ["unit"])
Tester avec le code suivant :
let t = remove_word t "caml" let () = assert (find t [2;2;6;5] = []) let () = assert (let l = find t [4;3] in l = ["hd"; "if"] || l = ["if"; "hd"]) let () = assert (find t [4;3;8] = ["get"])
% ocamlfind ocamlopt -package graphics -linkpkg quad.ml -o quadCeci produira l'exécutable quad. Cet exécutable dessine des images, puis s'arrête. L'affichage des images est contrôlé à l'aide des touches du clavier. À tout moment, la touche « q » (avec le curseur de la souris dans la fenêtre graphique) permet de passer à la question suivante.
Attention : la bibliothèque Graphics d'OCaml utilise le repère mathématique
y ^ | | 0 +-----> 0 xce qui est inhabituel pour une bibliothèque graphique.
Selon ce schéma, les images sont représentées par des arbres du type
suivant :
type color = White | Black type tree = | Leaf of color | Node of tree * tree * tree * treeOn manipulera aussi des images bitmap, représentées par des matrices de couleurs que l'on supposera toujours de côté 2n par la suite.
type image = color array array (* matrice de taille 2n *)
Les fonctions de test de quad.ml se chargent de la mise en place de la fenêtre et des appels à draw_tree. Normalement, vous deviez obtenir ce dessin :
(Si vous obtenez une isométrie quelconque de ce dessin,
tranquilisez-vous et continuez.)
Un test est inclus dans quad.ml, qui n'affiche rien.
(Il convient ici de bien regarder la première itération pour
comprendre le processus.)
Le code fourni permet d'avancer dans les itérations avec la touche n et de reculer avec la touche p. On sort du test en appuyant sur la touche q.
code(Leaf White) | = | 00 |
code(Leaf Black) | = | 01 |
code(Node (a1,a2,a3,a4)) | = | 1 code(a1) code(a2) code(a3) code(a4) |
Ainsi, l'arbre Node(Leaf White, Leaf Black, Leaf Black, Leaf Black) est encodé par la séquence 100010101 de longueur 9.
Pour simplifier on va procéder en deux temps : d'abord le codage (et
décodage) de
l'arbre en liste de bits ; puis l'écriture (et lecture) des bits dans
un fichier.
type bit = Zero | OneÉcrire une fonction tree_to_list de type tree -> bit list qui transforme un arbre en une liste de 0 et de 1 selon le codage ci-dessus.
Écrire la fonction réciproque list_to_tree de type bit list -> tree qui transforme une liste de bits en l'arbre correspondant. Cette fonction ressemble à l'analyse syntaxique d'une expresssion donnée sous forme préfixe, le codage choisi évitant les ambiguïtés.
Il faut faire un choix pour l'ordre des 8 bits à l'intérieur d'un même octet. On fait ici le choix de commencer par les bits de poids faible. Ainsi, la séquence 100010101 correspond à deux octets :
Les fonctions sur les fichiers sont regroupées dans le
module Stdlib (ouvert par défaut, ainsi on n'est pas obligé
de faire précéder ces fonctions de Stdlib.).
On portera un intérêt tout particulier à l'ouverture
(open_out et open_in) et à la fermeture
(close_out et close_in)
d'un fichier, ainsi qu'à l'écriture et à la lecture d'un caractère
(output_char et input_char).
Le squelette fourni contient un test (sauver et relire une des
fractales de la question précédente), mais vous pouvez aussi lire
cette image.