TD 1 - Mise à niveau Caml

Pour ce premier TD, il est suggéré de travailler dans Emacs, en utilisant le mode interactif d'Ocaml. Pour cela, ouvrez un fichier portant le suffixe .ml, par exemple en lançant la commande emacs td1.ml ou, si Emacs est déjà lancé, en ouvrant un fichier td1.ml avec Ctrl-x Ctrl-f. Vous devriez voir apparaître Tuareg en bas de la fenêtre, qui signale le mode Emacs pour Ocaml. Chaque déclaration que vous écrivez dans td1.ml peut être envoyée à Ocaml avec le raccourci Ctrl-x Ctrl-e. Si besoin, l'intégralité du fichier peut être envoyée à Ocaml avec le raccourci Ctrl-c Ctrl-b. (Toutes ces commandes se retrouvent dans le menu Tuareg.)

Il va sans dire que vous êtes libres d'utiliser un autre éditeur de texte, à partir du moment où vous le maîtrisez et êtes à même de développer efficacement en Ocaml avec cet éditeur.

Ceux d'entre vous qui se sentent à l'aise avec Caml peuvent attaquer directement à la question tri par tas.

Tri

Dans ce premier exercice, nous allons programmer quelques tris classiques, sous la forme de fonctions opérant sur des listes d'entiers, c'est-à-dire ayant le type int list -> int list.

Tri par insertion

Écrire une fonction insertion : int -> int list -> int list qui insère un élément dans une liste d'entiers supposée triée en ordre croissant.

É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).

Tester avec les exemples ci-dessous :

  insertion_sort [1; 2; 3];;
  insertion_sort [3; 2; 1];;
  insertion_sort [];;
  insertion_sort [1];;
  insertion_sort [2; 1; 1;];;
solution

Tri fusion (mergesort)

Écrire une fonction split : int list -> int list * int list qui prend une liste d'entiers l en argument et la « coupe en deux » c'est-à-dire renvoie une paire de liste (l1,l2) telle que les éléments de l sont exactement ceux de l1 et de l2 et telle que les listes l1 et l2 aient une longueur ne différant au plus d'un.

Écrire une fonction fusion : int list * int list -> int list prenant en arguments deux liste 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.

solution

Tri rapide (quicksort)

Écrire une fonction partition : int -> int list -> int list * int list qui prend en argument un entier x et une liste d'entiers l et renvoie une paire de listes (l1,l2) telle que l1 (resp. l2) contient les éléments de l plus petits (resp. plus grands) que x.

É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.

solution

Tri par tas (heapsort)

Structure de tas

Un tas est un arbre binaire contenant des entiers en chacun de ses noeuds et dont chaque sous-arbre t vérifie la propriété suivante : l'entier à la racine de t est inférieur ou égal à tous les entiers contenus dans t. Voici un exemple de tas contenant les entiers 1, 2, 2, 4, 5 et 6 :
           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 * heap
On 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 retourne le tas constitué par tous les autres éléments. Lorsque le tas est vide, on pourra lever l'exception EmptyHeap ainsi déclarée :

exception EmptyHeap

Note : ces tas s'appellent des skew heaps et sont à la fois persistants, élégants et très efficaces.

Application au tri

Une fois la structure de tas donnée, il est aisé d'en déduire un algorithme de tri, appelé tri par tas.

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.

solution

Paramétricité

Les fonctions de tri précédentes ne s'appliquent qu'à des listes d'entiers. On souhaite les généraliser afin qu'elles s'appliquent à des listes quelconques, dès lors que l'on se donne un ordre total sur leurs éléments.
  1. Une première solution consiste à utiliser l'ordre supérieur, pour passer la fonction de comparaison en argument. Reprendre le code de l'une des fonctions de tri ci-dessus et en faire une fonction de type
      sort : ('a -> 'a -> bool) -> 'a list -> 'a list
    
    où la fonction passée en argument correspond à la relation « inférieur ou égal à ». On pourra tester ainsi :
      sort (<=) [1;2;3;2;4;1];;
    
    solution
  2. Une autre solution consiste à écrire la fonction de tri à l'intérieur d'un foncteur, dont l'argument spécifie le type des éléments de la liste et la fonction de comparaison. Reprendre le code de l'une des fonctions de tri et en faire un foncteur dont la signature est
      module Sort(X : sig type t val le : t -> t -> bool end) : sig
        val sort : X.t list -> X.t list
      end
    
    On pourra tester ainsi :
      module S = Sort(struct type t = int let le = (<=) end);;
      S.sort [1;2;3;2;4;1];;
    
    solution

Le mode T9 des téléphones portables

Le mode T9 des téléphones portables facilite la saisie des textes sur les claviers : au lieu de taper plusieurs fois sur une touche pour faire défiler les lettres, une seule frappe suffit et le téléphone propose de lui-même les mots qui correspondent à la séquence de touches qui vient d'être tapée, à partir d'un dictionnaire qu'il a en mémoire.

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 digital (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 digital, on se donne le type Caml 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 digitaux à des entiers (on appelle précisément cela une liste d'association).
  1. Définir la valeur empty : trie correspondant au dictionnaire vide.

  2. Écrire une fonction find : trie -> int list -> string list renvoyant la liste des mots stockée dans le noeud désigné par la liste d'entiers passée en argument. (On pourra se servir de la fonction List.assoc de la bibliothèque Caml.)

  3. Écrire une fonction add : trie -> int list -> string -> trie ajoutant un mot dans le dictionnaire, la liste d'entiers correspondant au mot étant passée en argument. On pourra commencer par écrire une fonction
    change_assoc : 'a -> 'b -> ('a * 'b) list -> ('a * 'b) list
    
    qui modifie (ou étend) une liste d'association.

  4. Écrire une fonction key : char -> int qui associe aux lettres minuscules de l'alphabet le chiffre correspondant sur le clavier d'un téléphone portable. En déduire une fonction intlist_of_string : string -> int list qui convertit un mot en la liste d'entiers correspondant à sa saisie sur le clavier du téléphone, puis une fonction add_word : trie -> string -> trie ajoutant un mot dans le dictionnaire.

  5. Construire le dictionnaire correspondant à la liste de mots suivante :
    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"; "caml"; "bash";
      "ocaml"; "kde"; "gtk" ; "gcc"
    ]
    
    (On pourra utiliser pour cela la fonction de bibliothèque List.fold_left.)

    Tester avec les exemples suivants (la réponse attendue est indiquée entre parenthèses) :

    let _ = find t [2;2;6;5];; (* ["caml"] *)
    let _ = find t [4;3];;     (* ["hd"; "if"] *)
    let _ = find t [4;3;8];;   (* ["get"] *)
    let _ = find t [8;6;4;3];; (* ["void"] *)
    let _ = find t [8;6;4;8];; (* ["unit"] *)
    

  6. Écrire une fonction remove_word : trie -> string -> trie permettant de supprimer un mot du dictionnaire.
solution

retour à la page du cours